@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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +628 -190
- package/README.md +23 -9
- package/dist/auditLog-6WDBDNYL.js +8 -0
- package/dist/auditLog-HGPVDSDC.js +8 -0
- package/dist/auditLog-TDIKFBM4.js +8 -0
- package/dist/auditLog-XC2KY3ZZ.js +8 -0
- package/dist/chunk-2I235WNB.js +133 -0
- package/dist/chunk-2JWDGXTR.js +42 -0
- package/dist/chunk-2NOB6W2B.js +133 -0
- package/dist/chunk-3LFKVKKL.js +7199 -0
- package/dist/chunk-3U6WHPDX.js +4695 -0
- package/dist/chunk-3UCL6RYE.js +7272 -0
- package/dist/chunk-4GN2UQLI.js +130 -0
- package/dist/chunk-4MW33MZD.js +516 -0
- package/dist/chunk-4PUJBUKZ.js +4716 -0
- package/dist/chunk-4QHK6H6O.js +130 -0
- package/dist/chunk-55CQIHCO.js +133 -0
- package/dist/chunk-5LWINFWI.js +676 -0
- package/dist/chunk-5OKGLNQW.js +439 -0
- package/dist/chunk-5T6ETZEO.js +6183 -0
- package/dist/chunk-5WGIFUVL.js +4234 -0
- package/dist/chunk-66EW47T3.js +4237 -0
- package/dist/chunk-6TXUOTT2.js +581 -0
- package/dist/chunk-6YUYSYDA.js +130 -0
- package/dist/chunk-74VPNFMX.js +133 -0
- package/dist/chunk-77CGJRGV.js +6188 -0
- package/dist/chunk-7DS3VT4C.js +7135 -0
- package/dist/chunk-7FHX3VBT.js +133 -0
- package/dist/chunk-7MHF56YU.js +6178 -0
- package/dist/chunk-ABLPMV7G.js +133 -0
- package/dist/chunk-B77K6OQZ.js +687 -0
- package/dist/chunk-BKOXH66O.js +133 -0
- package/dist/chunk-BPP76UN2.js +130 -0
- package/dist/chunk-C4D56GRC.js +5936 -0
- package/dist/chunk-CCDPY4WE.js +370 -0
- package/dist/chunk-CHNVBJN3.js +7272 -0
- package/dist/chunk-CKBZI576.js +7229 -0
- package/dist/chunk-CW5HBSJ2.js +7198 -0
- package/dist/chunk-DGHDJEY7.js +133 -0
- package/dist/chunk-DPNIQWKZ.js +439 -0
- package/dist/chunk-DU4T3V2T.js +214 -0
- package/dist/chunk-DV3WFKNB.js +4679 -0
- package/dist/chunk-DZI2OVN2.js +516 -0
- package/dist/chunk-E45VILML.js +7198 -0
- package/dist/chunk-ECEUUYXC.js +7199 -0
- package/dist/chunk-EJRRSHPW.js +685 -0
- package/dist/chunk-EMBMLZFE.js +370 -0
- package/dist/chunk-EPG5V5OO.js +285 -0
- package/dist/chunk-F2R4HXXW.js +130 -0
- package/dist/chunk-FK6N66ES.js +581 -0
- package/dist/chunk-G3CZKGYA.js +197 -0
- package/dist/chunk-GUUPG4A7.js +7111 -0
- package/dist/chunk-HBAAUGUN.js +7230 -0
- package/dist/chunk-HHFJMK2Q.js +6177 -0
- package/dist/chunk-HINRQTCZ.js +196 -0
- package/dist/chunk-HRKJ3R2U.js +288 -0
- package/dist/chunk-HWVK4CVE.js +439 -0
- package/dist/chunk-JEYSADNZ.js +581 -0
- package/dist/chunk-JNEIL7UN.js +4252 -0
- package/dist/chunk-JTWSK277.js +676 -0
- package/dist/chunk-K4CHTTCJ.js +942 -0
- package/dist/chunk-K7R5KUDS.js +4695 -0
- package/dist/chunk-KNJFOURE.js +7151 -0
- package/dist/chunk-KY22FIT3.js +7256 -0
- package/dist/chunk-L2OTIJSF.js +4228 -0
- package/dist/chunk-LEEBUHP6.js +4655 -0
- package/dist/chunk-LK7UP2T7.js +130 -0
- package/dist/chunk-LPGNO3PK.js +284 -0
- package/dist/chunk-LYQYJMWS.js +133 -0
- package/dist/chunk-MBYFJXR3.js +130 -0
- package/dist/chunk-N3WX44L3.js +130 -0
- package/dist/chunk-N6AQWES3.js +6197 -0
- package/dist/chunk-NW4XSTQZ.js +130 -0
- package/dist/chunk-NWG2XURH.js +130 -0
- package/dist/chunk-O3GF3LJD.js +6142 -0
- package/dist/chunk-OHP5LD3Y.js +6188 -0
- package/dist/chunk-P5PQSFZT.js +6182 -0
- package/dist/chunk-PAADOWNP.js +130 -0
- package/dist/chunk-PERGND7L.js +7213 -0
- package/dist/chunk-PWA7V4XX.js +179 -0
- package/dist/chunk-QGCWEP6L.js +7111 -0
- package/dist/chunk-QVT2IHNJ.js +175 -0
- package/dist/chunk-QZNGYPMS.js +6161 -0
- package/dist/chunk-R6P2E2ZQ.js +207 -0
- package/dist/chunk-ROSDMGIL.js +4679 -0
- package/dist/chunk-RQZP7IKG.js +196 -0
- package/dist/chunk-RUQSPX3U.js +133 -0
- package/dist/chunk-S3BYHP5M.js +130 -0
- package/dist/chunk-S6GNETVE.js +438 -0
- package/dist/chunk-SDT2ZE2R.js +133 -0
- package/dist/chunk-SHQBXJFC.js +6166 -0
- package/dist/chunk-TJNISYTE.js +42 -0
- package/dist/chunk-TJW74HFF.js +130 -0
- package/dist/chunk-TPP72DTK.js +7096 -0
- package/dist/chunk-UOESII6R.js +42 -0
- package/dist/chunk-UWEDGLYJ.js +6142 -0
- package/dist/chunk-V5FYNAFX.js +133 -0
- package/dist/chunk-VPURF6UT.js +7198 -0
- package/dist/chunk-VQH6LWIZ.js +6184 -0
- package/dist/chunk-VS22YVX6.js +7111 -0
- package/dist/chunk-VSF5KBW7.js +367 -0
- package/dist/chunk-VV3JMCKY.js +214 -0
- package/dist/chunk-W5L7HOE3.js +133 -0
- package/dist/chunk-WFEVQISK.js +676 -0
- package/dist/chunk-WJZPSCEP.js +516 -0
- package/dist/chunk-WLV4MKEF.js +16 -0
- package/dist/chunk-WSEVQFFI.js +5428 -0
- package/dist/chunk-X7N2RNR3.js +5428 -0
- package/dist/chunk-XKZNMRNO.js +133 -0
- package/dist/chunk-Y7BVEC36.js +130 -0
- package/dist/chunk-YG7YSNNU.js +4226 -0
- package/dist/chunk-YHM62466.js +261 -0
- package/dist/chunk-YLTYJLDZ.js +7208 -0
- package/dist/chunk-YPMJD4YE.js +56 -0
- package/dist/chunk-YTX3FU2A.js +7199 -0
- package/dist/chunk-ZEQ3EBBN.js +214 -0
- package/dist/chunk-ZIWLQSLK.js +42 -0
- package/dist/chunk-ZJELNTEO.js +516 -0
- package/dist/chunk-ZOSSVNGK.js +370 -0
- package/dist/config-EYK32F2E.js +10 -0
- package/dist/config-FJPPPYTY.js +10 -0
- package/dist/config-VAHPVILX.js +10 -0
- package/dist/context-2YGE4U75.js +10 -0
- package/dist/context-5UFVYKES.js +9 -0
- package/dist/context-ANZF4J72.js +10 -0
- package/dist/context-GLUNCUBQ.js +10 -0
- package/dist/context-M5ULPZKQ.js +10 -0
- package/dist/context-NYOIRZKV.js +10 -0
- package/dist/context-YP2REI6A.js +10 -0
- package/dist/database-MP2JBLMF.js +8 -0
- package/dist/index.js +6177 -4688
- package/dist/keyManager-P2SZONKE.js +8 -0
- package/dist/mcp/index.js +127 -47
- package/dist/memory-BPGJAL4J.js +13 -0
- package/dist/memory-FRQOUI6W.js +13 -0
- package/dist/memory-OAMK27IZ.js +13 -0
- package/dist/memory-OZ734ALL.js +13 -0
- package/dist/memory-PQ2EWRMU.js +13 -0
- package/dist/memory-PXL45M6W.js +13 -0
- package/dist/memory-QZTBTYPH.js +13 -0
- package/dist/migrations-TLJ3WRVW.js +188 -0
- package/dist/resume-2NHDK6EI.js +17 -0
- package/dist/resume-44L2PDB2.js +17 -0
- package/dist/resume-4MXIWUJO.js +7 -0
- package/dist/resume-72VJX66I.js +17 -0
- package/dist/resume-7ZW4XM3B.js +15 -0
- package/dist/resume-AZHYQ657.js +17 -0
- package/dist/resume-CNLXSYHV.js +17 -0
- package/dist/resume-DPN4Q777.js +16 -0
- package/dist/resume-DSFHVNPI.js +15 -0
- package/dist/resume-GHLQJJTO.js +17 -0
- package/dist/resume-HJ6SBWTF.js +17 -0
- package/dist/resume-HRLYHY2L.js +17 -0
- package/dist/resume-ISIQFKO6.js +17 -0
- package/dist/resume-JQDEA6PS.js +15 -0
- package/dist/resume-KP3Y3Y7P.js +17 -0
- package/dist/resume-M4KHR5OI.js +17 -0
- package/dist/resume-N62OAMBG.js +17 -0
- package/dist/resume-NNMA5POT.js +17 -0
- package/dist/resume-ODYT3J4H.js +17 -0
- package/dist/resume-PCFJXA5O.js +17 -0
- package/dist/resume-PLF4XGBD.js +15 -0
- package/dist/resume-Q2PFX57V.js +17 -0
- package/dist/resume-QB4XI2J5.js +17 -0
- package/dist/resume-R5MFTUPF.js +17 -0
- package/dist/resume-SVA7223Z.js +17 -0
- package/dist/resume-TYKKDJZI.js +17 -0
- package/dist/resume-XIS45HKV.js +17 -0
- package/dist/resume-YCSEJTU7.js +17 -0
- package/dist/resume-YD76GI2J.js +15 -0
- package/dist/resume-YDN7EL77.js +17 -0
- package/dist/resume-YE7DB4ZA.js +17 -0
- package/dist/resume-YKAKOXWV.js +15 -0
- package/dist/resume-ZHBCVFDY.js +17 -0
- package/dist/scheduler-2CK24A2Q.js +14 -0
- package/dist/scheduler-7OAF2XKX.js +14 -0
- package/dist/scheduler-AS23AAB5.js +14 -0
- package/dist/scheduler-PCOYQJA5.js +14 -0
- package/dist/scheduler-V2ECBQPK.js +14 -0
- package/dist/scheduler-VEWZ6L7V.js +13 -0
- package/dist/scheduler-W37QMGDQ.js +14 -0
- package/dist/session-2E2JKPD7.js +15 -0
- package/dist/session-2VSF257B.js +14 -0
- package/dist/session-4APFTDJU.js +14 -0
- package/dist/session-5YS5LNNL.js +16 -0
- package/dist/session-6MO5ZPOB.js +16 -0
- package/dist/session-6XMGPRTQ.js +14 -0
- package/dist/session-7DJR77R7.js +16 -0
- package/dist/session-7DQHPWTR.js +14 -0
- package/dist/session-ADKIQCR5.js +16 -0
- package/dist/session-AOGH2GGI.js +16 -0
- package/dist/session-C4W6GDYG.js +16 -0
- package/dist/session-DCGNGGMV.js +14 -0
- package/dist/session-F5JKZAN2.js +16 -0
- package/dist/session-G6F3O2FQ.js +16 -0
- package/dist/session-GFBSARRO.js +16 -0
- package/dist/session-H5IWAIUI.js +16 -0
- package/dist/session-IPFA6AHC.js +14 -0
- package/dist/session-IWG2UOAX.js +14 -0
- package/dist/session-KJ2K4Y4M.js +14 -0
- package/dist/session-KPXFBW6Q.js +14 -0
- package/dist/session-KR256UL5.js +16 -0
- package/dist/session-M72LJXPR.js +16 -0
- package/dist/session-MBK3FODN.js +14 -0
- package/dist/session-MOUFAU7G.js +16 -0
- package/dist/session-NRC6ZXFQ.js +16 -0
- package/dist/session-NRPQMV4K.js +16 -0
- package/dist/session-O5IFFJZQ.js +14 -0
- package/dist/session-OF5BGKDE.js +16 -0
- package/dist/session-OGRZMIM7.js +14 -0
- package/dist/session-OJOFAJG3.js +16 -0
- package/dist/session-OKU4N3SP.js +16 -0
- package/dist/session-P2VAOSFB.js +14 -0
- package/dist/session-PKOVZD4M.js +16 -0
- package/dist/session-POAIMUVN.js +16 -0
- package/dist/session-PSHFONFE.js +16 -0
- package/dist/session-QKYVVZFV.js +16 -0
- package/dist/session-QPWGBMUS.js +14 -0
- package/dist/session-R5UG5PZR.js +14 -0
- package/dist/session-RAY6BZRQ.js +16 -0
- package/dist/session-S3VATHMU.js +16 -0
- package/dist/session-SYTD7RHW.js +14 -0
- package/dist/session-UHMMVO4J.js +16 -0
- package/dist/session-WEX5K3ZY.js +14 -0
- package/dist/session-XFLOXGU3.js +14 -0
- package/dist/session-XV2A4HHG.js +14 -0
- package/dist/settings-3VPJYD4D.js +8 -0
- package/dist/settings-GZTJJTBK.js +8 -0
- package/dist/settings-YKJFSKMO.js +8 -0
- package/dist/shell-FM34624T.js +8 -0
- package/package.json +14 -4
|
@@ -0,0 +1,4226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scheduler
|
|
3
|
+
} from "./chunk-XOMN4VJS.js";
|
|
4
|
+
import {
|
|
5
|
+
auditLog,
|
|
6
|
+
redactor
|
|
7
|
+
} from "./chunk-5SWSM5VM.js";
|
|
8
|
+
import {
|
|
9
|
+
context
|
|
10
|
+
} from "./chunk-MSDU36RT.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-7RCNGBCH.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
|
+
console.error("Failed to load task:", 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
|
+
console.error("Failed to save task:", 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/tools.ts
|
|
1455
|
+
var execAsync2 = promisify2(exec2);
|
|
1456
|
+
var lastScreenshotScale = 1;
|
|
1457
|
+
var MAX_OUTPUT_LENGTH = 1e4;
|
|
1458
|
+
var MAX_FILE_READ_LINES = 500;
|
|
1459
|
+
var IGNORED_DIRS = ["node_modules", ".git", "dist", ".next", "__pycache__", ".cache", "coverage"];
|
|
1460
|
+
var APPROVAL_TIMEOUT = 3e4;
|
|
1461
|
+
function truncateOutput(output, maxLength = MAX_OUTPUT_LENGTH) {
|
|
1462
|
+
if (output.length <= maxLength) return output;
|
|
1463
|
+
const truncated = output.slice(0, maxLength);
|
|
1464
|
+
const remaining = output.length - maxLength;
|
|
1465
|
+
return `${truncated}
|
|
1466
|
+
|
|
1467
|
+
... [TRUNCATED: ${remaining} more characters]`;
|
|
1468
|
+
}
|
|
1469
|
+
function filterSystemNoise(stderr) {
|
|
1470
|
+
if (!stderr) return stderr;
|
|
1471
|
+
const noisePatterns = [
|
|
1472
|
+
/^aks:aks_get_lock_state:\d+:\d+: aks connection failed\s*/gm,
|
|
1473
|
+
// macOS keychain noise
|
|
1474
|
+
/^objc\[\d+\]: .* may have been in progress in another thread.*$/gm,
|
|
1475
|
+
// Objective-C runtime
|
|
1476
|
+
/^Warning: .* is deprecated.*$/gm,
|
|
1477
|
+
// Deprecation warnings
|
|
1478
|
+
/^\[warn\].*$/gmi,
|
|
1479
|
+
// Generic warn prefixes
|
|
1480
|
+
/^MESA-LOADER:.*$/gm,
|
|
1481
|
+
// Mesa graphics loader
|
|
1482
|
+
/^libEGL warning:.*$/gm,
|
|
1483
|
+
// EGL warnings
|
|
1484
|
+
/^Fontconfig warning:.*$/gm
|
|
1485
|
+
// Font config
|
|
1486
|
+
];
|
|
1487
|
+
let filtered = stderr;
|
|
1488
|
+
for (const pattern of noisePatterns) {
|
|
1489
|
+
filtered = filtered.replace(pattern, "");
|
|
1490
|
+
}
|
|
1491
|
+
filtered = filtered.replace(/^\s*[\r\n]/gm, "").trim();
|
|
1492
|
+
return filtered;
|
|
1493
|
+
}
|
|
1494
|
+
var pendingApprovals = /* @__PURE__ */ new Map();
|
|
1495
|
+
bus.on("user", (event) => {
|
|
1496
|
+
if (event.type === "approval_response") {
|
|
1497
|
+
const pending = pendingApprovals.get(event.requestId);
|
|
1498
|
+
if (pending) {
|
|
1499
|
+
clearTimeout(pending.timeout);
|
|
1500
|
+
pendingApprovals.delete(event.requestId);
|
|
1501
|
+
pending.resolve({ approved: event.approved, scope: event.scope, bypass: event.bypass });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
async function requestApproval(command, reason) {
|
|
1506
|
+
const requestId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1507
|
+
return new Promise((resolve) => {
|
|
1508
|
+
const timeout = setTimeout(() => {
|
|
1509
|
+
pendingApprovals.delete(requestId);
|
|
1510
|
+
bus.emitAgent({
|
|
1511
|
+
type: "error",
|
|
1512
|
+
message: "No response received. Command blocked for safety."
|
|
1513
|
+
});
|
|
1514
|
+
resolve({ approved: false, scope: "session" });
|
|
1515
|
+
}, APPROVAL_TIMEOUT);
|
|
1516
|
+
pendingApprovals.set(requestId, { resolve, timeout });
|
|
1517
|
+
const context2 = [
|
|
1518
|
+
`Command: ${command}`,
|
|
1519
|
+
`Reason: ${reason}`
|
|
1520
|
+
].join("\n");
|
|
1521
|
+
bus.emitAgent({
|
|
1522
|
+
type: "approval_request",
|
|
1523
|
+
requestId,
|
|
1524
|
+
context: context2
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
var BashTool = {
|
|
1529
|
+
name: "bash",
|
|
1530
|
+
description: "Execute shell commands in the workspace",
|
|
1531
|
+
inputSchema: {
|
|
1532
|
+
command: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
description: "The shell command to execute"
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
requiredParams: ["command"],
|
|
1538
|
+
async execute(args) {
|
|
1539
|
+
const command = args.command;
|
|
1540
|
+
if (!command) {
|
|
1541
|
+
return { success: false, error: "No command provided" };
|
|
1542
|
+
}
|
|
1543
|
+
const audit = await auditor.checkCommand(command);
|
|
1544
|
+
if (!audit.approved && audit.isCritical) {
|
|
1545
|
+
await auditLog.logSecurityViolation(command, audit.reason || "Critical security violation");
|
|
1546
|
+
return {
|
|
1547
|
+
success: false,
|
|
1548
|
+
error: `Security violation: ${audit.reason}`
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
if (!audit.approved && audit.requiresApproval) {
|
|
1552
|
+
await auditLog.logApproval("requested", command, audit.reason);
|
|
1553
|
+
const { approved, scope, bypass } = await requestApproval(command, audit.reason || "Potentially dangerous operation");
|
|
1554
|
+
if (approved) {
|
|
1555
|
+
if (scope === "persistent") {
|
|
1556
|
+
if (bypass) {
|
|
1557
|
+
await settings.addUnsandboxedPermission("bash", command);
|
|
1558
|
+
} else {
|
|
1559
|
+
await settings.addAllowedPermission("bash", command);
|
|
1560
|
+
}
|
|
1561
|
+
} else {
|
|
1562
|
+
await settings.addSessionPermission("bash", command, true, bypass);
|
|
1563
|
+
}
|
|
1564
|
+
await auditLog.logApproval("granted", command, bypass ? "Bypass enabled" : void 0);
|
|
1565
|
+
} else {
|
|
1566
|
+
if (scope === "persistent") {
|
|
1567
|
+
await settings.addDeniedPermission("bash", command);
|
|
1568
|
+
} else {
|
|
1569
|
+
await settings.addSessionPermission("bash", command, false);
|
|
1570
|
+
}
|
|
1571
|
+
await auditLog.logApproval("denied", command);
|
|
1572
|
+
return {
|
|
1573
|
+
success: false,
|
|
1574
|
+
error: "Command rejected by user"
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const bypassSandbox = await settings.isUnsandboxed("bash", command);
|
|
1579
|
+
const cfg = await config.load();
|
|
1580
|
+
try {
|
|
1581
|
+
const execCommand = await sandbox.wrapCommand(command, bypassSandbox);
|
|
1582
|
+
const { stdout, stderr } = await execAsync2(execCommand, {
|
|
1583
|
+
cwd: cfg.workspaceRoot,
|
|
1584
|
+
timeout: 3e4,
|
|
1585
|
+
// 30 second timeout
|
|
1586
|
+
maxBuffer: 1024 * 1024
|
|
1587
|
+
// 1MB buffer (reduced from 10MB)
|
|
1588
|
+
});
|
|
1589
|
+
const filteredStderr = filterSystemNoise(stderr);
|
|
1590
|
+
const output = stdout || filteredStderr || "Command executed successfully";
|
|
1591
|
+
await auditLog.logCommand(command, true);
|
|
1592
|
+
return {
|
|
1593
|
+
success: true,
|
|
1594
|
+
output: truncateOutput(output)
|
|
1595
|
+
};
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
await auditLog.logCommand(command, false, error.message);
|
|
1598
|
+
return {
|
|
1599
|
+
success: false,
|
|
1600
|
+
error: error.message || "Command execution failed"
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
var ReadTool = {
|
|
1606
|
+
name: "read",
|
|
1607
|
+
description: "Read file contents from the workspace",
|
|
1608
|
+
inputSchema: {
|
|
1609
|
+
path: {
|
|
1610
|
+
type: "string",
|
|
1611
|
+
description: "Path to the file to read (relative to workspace)"
|
|
1612
|
+
}
|
|
1613
|
+
},
|
|
1614
|
+
requiredParams: ["path"],
|
|
1615
|
+
async execute(args) {
|
|
1616
|
+
const filePath = args.path;
|
|
1617
|
+
if (!filePath) {
|
|
1618
|
+
return { success: false, error: "No file path provided" };
|
|
1619
|
+
}
|
|
1620
|
+
const pathCheck = auditor.checkPath(filePath);
|
|
1621
|
+
if (!pathCheck.approved) {
|
|
1622
|
+
return {
|
|
1623
|
+
success: false,
|
|
1624
|
+
error: pathCheck.reason
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
if (IGNORED_DIRS.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) {
|
|
1628
|
+
return {
|
|
1629
|
+
success: false,
|
|
1630
|
+
error: `Cannot read from ignored directory. Paths containing ${IGNORED_DIRS.join(", ")} are blocked to prevent context explosion.`
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
try {
|
|
1634
|
+
const cfg = await config.load();
|
|
1635
|
+
const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
|
|
1636
|
+
const content = await fs5.readFile(fullPath, "utf-8");
|
|
1637
|
+
const lines = content.split("\n");
|
|
1638
|
+
const limitedLines = lines.slice(0, MAX_FILE_READ_LINES);
|
|
1639
|
+
const numbered = limitedLines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
|
|
1640
|
+
const truncationNote = lines.length > MAX_FILE_READ_LINES ? `
|
|
1641
|
+
|
|
1642
|
+
... [TRUNCATED: ${lines.length - MAX_FILE_READ_LINES} more lines. Use offset parameter to read more.]` : "";
|
|
1643
|
+
await context.trackRead(filePath);
|
|
1644
|
+
await auditLog.logFileOperation("read", filePath, true);
|
|
1645
|
+
return {
|
|
1646
|
+
success: true,
|
|
1647
|
+
output: truncateOutput(`File: ${filePath} (${lines.length} lines)
|
|
1648
|
+
${"=".repeat(60)}
|
|
1649
|
+
${numbered}${truncationNote}`)
|
|
1650
|
+
};
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
await auditLog.logFileOperation("read", filePath, false, error.message);
|
|
1653
|
+
return {
|
|
1654
|
+
success: false,
|
|
1655
|
+
error: `Failed to read file: ${error.message}`
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
var WriteTool = {
|
|
1661
|
+
name: "write",
|
|
1662
|
+
description: "Create new files in the workspace",
|
|
1663
|
+
inputSchema: {
|
|
1664
|
+
path: {
|
|
1665
|
+
type: "string",
|
|
1666
|
+
description: "Path where to create the new file"
|
|
1667
|
+
},
|
|
1668
|
+
content: {
|
|
1669
|
+
type: "string",
|
|
1670
|
+
description: "Content to write to the file"
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
requiredParams: ["path", "content"],
|
|
1674
|
+
async execute(args) {
|
|
1675
|
+
const filePath = args.path;
|
|
1676
|
+
const content = args.content;
|
|
1677
|
+
if (!filePath) {
|
|
1678
|
+
return { success: false, error: "No file path provided" };
|
|
1679
|
+
}
|
|
1680
|
+
if (content === void 0) {
|
|
1681
|
+
return { success: false, error: "No content provided" };
|
|
1682
|
+
}
|
|
1683
|
+
const pathCheck = auditor.checkPath(filePath);
|
|
1684
|
+
if (!pathCheck.approved) {
|
|
1685
|
+
return {
|
|
1686
|
+
success: false,
|
|
1687
|
+
error: pathCheck.reason
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
try {
|
|
1691
|
+
const cfg = await config.load();
|
|
1692
|
+
const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
|
|
1693
|
+
try {
|
|
1694
|
+
await fs5.access(fullPath);
|
|
1695
|
+
return {
|
|
1696
|
+
success: false,
|
|
1697
|
+
error: `File already exists: ${filePath}. Use 'edit' tool to modify.`
|
|
1698
|
+
};
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
await fs5.mkdir(path5.dirname(fullPath), { recursive: true });
|
|
1702
|
+
await fs5.writeFile(fullPath, content, "utf-8");
|
|
1703
|
+
await context.trackModified(filePath);
|
|
1704
|
+
await undo.recordChange(filePath, "create", null, content);
|
|
1705
|
+
await auditLog.logFileOperation("write", filePath, true);
|
|
1706
|
+
await diffManager.saveDiff(filePath, "", content);
|
|
1707
|
+
return {
|
|
1708
|
+
success: true,
|
|
1709
|
+
output: `Created file: ${filePath} (${content.length} bytes)`
|
|
1710
|
+
};
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
await auditLog.logFileOperation("write", filePath, false, error.message);
|
|
1713
|
+
return {
|
|
1714
|
+
success: false,
|
|
1715
|
+
error: `Failed to write file: ${error.message}`
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
var EditTool = {
|
|
1721
|
+
name: "edit",
|
|
1722
|
+
description: "Edit existing files using search and replace",
|
|
1723
|
+
inputSchema: {
|
|
1724
|
+
path: {
|
|
1725
|
+
type: "string",
|
|
1726
|
+
description: "Path to the file to edit"
|
|
1727
|
+
},
|
|
1728
|
+
search: {
|
|
1729
|
+
type: "string",
|
|
1730
|
+
description: "Text to search for (must match exactly)"
|
|
1731
|
+
},
|
|
1732
|
+
replace: {
|
|
1733
|
+
type: "string",
|
|
1734
|
+
description: "Text to replace with"
|
|
1735
|
+
}
|
|
1736
|
+
},
|
|
1737
|
+
requiredParams: ["path", "search", "replace"],
|
|
1738
|
+
async execute(args) {
|
|
1739
|
+
const filePath = args.path;
|
|
1740
|
+
const search = args.search;
|
|
1741
|
+
const replace = args.replace;
|
|
1742
|
+
if (!filePath) {
|
|
1743
|
+
return { success: false, error: "No file path provided" };
|
|
1744
|
+
}
|
|
1745
|
+
if (!search) {
|
|
1746
|
+
return { success: false, error: "No search string provided" };
|
|
1747
|
+
}
|
|
1748
|
+
if (replace === void 0) {
|
|
1749
|
+
return { success: false, error: "No replacement string provided" };
|
|
1750
|
+
}
|
|
1751
|
+
const fileCheck = await auditor.checkFileEdit(filePath);
|
|
1752
|
+
if (!fileCheck.approved) {
|
|
1753
|
+
return {
|
|
1754
|
+
success: false,
|
|
1755
|
+
error: fileCheck.reason
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
const cfg = await config.load();
|
|
1760
|
+
const fullPath = path5.resolve(cfg.workspaceRoot, filePath);
|
|
1761
|
+
const original = await fs5.readFile(fullPath, "utf-8");
|
|
1762
|
+
if (!original.includes(search)) {
|
|
1763
|
+
return {
|
|
1764
|
+
success: false,
|
|
1765
|
+
error: `Search string not found in ${filePath}`
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
const occurrences = original.split(search).length - 1;
|
|
1769
|
+
const modified = original.replaceAll(search, replace);
|
|
1770
|
+
const diffPreview = generateDiffPreview(search, replace);
|
|
1771
|
+
await fs5.writeFile(fullPath, modified, "utf-8");
|
|
1772
|
+
const originalLines = original.split("\n").length;
|
|
1773
|
+
const modifiedLines = modified.split("\n").length;
|
|
1774
|
+
const delta = modifiedLines - originalLines;
|
|
1775
|
+
await context.trackModified(filePath);
|
|
1776
|
+
await undo.recordChange(filePath, "edit", original, modified);
|
|
1777
|
+
await auditLog.logFileOperation("edit", filePath, true);
|
|
1778
|
+
await diffManager.saveDiff(filePath, original, modified);
|
|
1779
|
+
const occurrenceText = occurrences > 1 ? ` (${occurrences} occurrences)` : "";
|
|
1780
|
+
return {
|
|
1781
|
+
success: true,
|
|
1782
|
+
output: `Edited ${filePath}${occurrenceText}:
|
|
1783
|
+
${diffPreview}
|
|
1784
|
+
Lines: ${originalLines} -> ${modifiedLines} (${delta >= 0 ? "+" : ""}${delta})`
|
|
1785
|
+
};
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
await auditLog.logFileOperation("edit", filePath, false, error.message);
|
|
1788
|
+
return {
|
|
1789
|
+
success: false,
|
|
1790
|
+
error: `Failed to edit file: ${error.message}`
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
function generateDiffPreview(search, replace) {
|
|
1796
|
+
const searchLines = search.split("\n").slice(0, 5);
|
|
1797
|
+
const replaceLines = replace.split("\n").slice(0, 5);
|
|
1798
|
+
let preview = "";
|
|
1799
|
+
for (const line of searchLines) {
|
|
1800
|
+
preview += `- ${line}
|
|
1801
|
+
`;
|
|
1802
|
+
}
|
|
1803
|
+
if (search.split("\n").length > 5) {
|
|
1804
|
+
preview += `- ... (${search.split("\n").length - 5} more lines)
|
|
1805
|
+
`;
|
|
1806
|
+
}
|
|
1807
|
+
for (const line of replaceLines) {
|
|
1808
|
+
preview += `+ ${line}
|
|
1809
|
+
`;
|
|
1810
|
+
}
|
|
1811
|
+
if (replace.split("\n").length > 5) {
|
|
1812
|
+
preview += `+ ... (${replace.split("\n").length - 5} more lines)
|
|
1813
|
+
`;
|
|
1814
|
+
}
|
|
1815
|
+
return preview.trim();
|
|
1816
|
+
}
|
|
1817
|
+
var ListTool = {
|
|
1818
|
+
name: "list",
|
|
1819
|
+
description: "List files and directories in the workspace",
|
|
1820
|
+
inputSchema: {
|
|
1821
|
+
path: {
|
|
1822
|
+
type: "string",
|
|
1823
|
+
description: "Directory path to list (defaults to current directory)"
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
requiredParams: [],
|
|
1827
|
+
async execute(args) {
|
|
1828
|
+
const dirPath = args.path || ".";
|
|
1829
|
+
const pathCheck = auditor.checkPath(dirPath);
|
|
1830
|
+
if (!pathCheck.approved) {
|
|
1831
|
+
return {
|
|
1832
|
+
success: false,
|
|
1833
|
+
error: pathCheck.reason
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
const cfg = await config.load();
|
|
1838
|
+
const fullPath = path5.resolve(cfg.workspaceRoot, dirPath);
|
|
1839
|
+
const entries = await fs5.readdir(fullPath, { withFileTypes: true });
|
|
1840
|
+
const filtered = entries.filter(
|
|
1841
|
+
(entry) => !IGNORED_DIRS.includes(entry.name) && !entry.name.startsWith(".")
|
|
1842
|
+
);
|
|
1843
|
+
const formatted = filtered.map((entry) => {
|
|
1844
|
+
const prefix = entry.isDirectory() ? "[DIR]" : "[FILE]";
|
|
1845
|
+
return `${prefix} ${entry.name}`;
|
|
1846
|
+
}).join("\n");
|
|
1847
|
+
const hiddenCount = entries.length - filtered.length;
|
|
1848
|
+
const hiddenNote = hiddenCount > 0 ? `
|
|
1849
|
+
|
|
1850
|
+
(${hiddenCount} hidden: node_modules, .git, etc.)` : "";
|
|
1851
|
+
return {
|
|
1852
|
+
success: true,
|
|
1853
|
+
output: `Directory: ${dirPath}
|
|
1854
|
+
${"=".repeat(60)}
|
|
1855
|
+
${formatted}${hiddenNote}`
|
|
1856
|
+
};
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
return {
|
|
1859
|
+
success: false,
|
|
1860
|
+
error: `Failed to list directory: ${error.message}`
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
var GrepTool = {
|
|
1866
|
+
name: "grep",
|
|
1867
|
+
description: "Search for patterns in files using regex",
|
|
1868
|
+
inputSchema: {
|
|
1869
|
+
pattern: {
|
|
1870
|
+
type: "string",
|
|
1871
|
+
description: "Regex pattern to search for"
|
|
1872
|
+
},
|
|
1873
|
+
path: {
|
|
1874
|
+
type: "string",
|
|
1875
|
+
description: "Directory to search in (defaults to current directory)"
|
|
1876
|
+
},
|
|
1877
|
+
limit: {
|
|
1878
|
+
type: "number",
|
|
1879
|
+
description: "Maximum number of results (default: 50)"
|
|
1880
|
+
}
|
|
1881
|
+
},
|
|
1882
|
+
requiredParams: ["pattern"],
|
|
1883
|
+
async execute(args) {
|
|
1884
|
+
const pattern = args.pattern;
|
|
1885
|
+
const searchPath = args.path || ".";
|
|
1886
|
+
const maxResults = args.limit || 50;
|
|
1887
|
+
if (!pattern) {
|
|
1888
|
+
return { success: false, error: "No search pattern provided" };
|
|
1889
|
+
}
|
|
1890
|
+
const pathCheck = auditor.checkPath(searchPath);
|
|
1891
|
+
if (!pathCheck.approved) {
|
|
1892
|
+
return {
|
|
1893
|
+
success: false,
|
|
1894
|
+
error: pathCheck.reason
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
try {
|
|
1898
|
+
const cfg = await config.load();
|
|
1899
|
+
const fullPath = path5.resolve(cfg.workspaceRoot, searchPath);
|
|
1900
|
+
const results = [];
|
|
1901
|
+
await searchDirectory(fullPath, pattern, results, maxResults, 0, cfg.workspaceRoot);
|
|
1902
|
+
if (results.length === 0) {
|
|
1903
|
+
return {
|
|
1904
|
+
success: true,
|
|
1905
|
+
output: `No matches found for: ${pattern}`
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
return {
|
|
1909
|
+
success: true,
|
|
1910
|
+
output: truncateOutput(`Found ${results.length} matches for "${pattern}":
|
|
1911
|
+
${"=".repeat(60)}
|
|
1912
|
+
${results.join("\n")}`)
|
|
1913
|
+
};
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
return {
|
|
1916
|
+
success: false,
|
|
1917
|
+
error: `Search failed: ${error.message}`
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
async function searchDirectory(dir, pattern, results, maxResults, depth = 0, workspaceRoot = process.cwd()) {
|
|
1923
|
+
if (results.length >= maxResults || depth > 10) return;
|
|
1924
|
+
try {
|
|
1925
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1926
|
+
const regex = new RegExp(pattern, "gi");
|
|
1927
|
+
for (const entry of entries) {
|
|
1928
|
+
if (results.length >= maxResults) break;
|
|
1929
|
+
const fullPath = path5.join(dir, entry.name);
|
|
1930
|
+
const relativePath = path5.relative(workspaceRoot, fullPath);
|
|
1931
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (entry.isDirectory()) {
|
|
1935
|
+
await searchDirectory(fullPath, pattern, results, maxResults, depth + 1, workspaceRoot);
|
|
1936
|
+
} else if (entry.isFile()) {
|
|
1937
|
+
const ext = path5.extname(entry.name).toLowerCase();
|
|
1938
|
+
const textExtensions = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".yaml", ".yml", ".css", ".html", ".sh"];
|
|
1939
|
+
if (textExtensions.includes(ext) || ext === "") {
|
|
1940
|
+
try {
|
|
1941
|
+
const content = await fs5.readFile(fullPath, "utf-8");
|
|
1942
|
+
const lines = content.split("\n");
|
|
1943
|
+
for (let i = 0; i < lines.length && results.length < maxResults; i++) {
|
|
1944
|
+
if (regex.test(lines[i])) {
|
|
1945
|
+
results.push(`${relativePath}:${i + 1}: ${lines[i].trim().slice(0, 100)}`);
|
|
1946
|
+
}
|
|
1947
|
+
regex.lastIndex = 0;
|
|
1948
|
+
}
|
|
1949
|
+
} catch {
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
var GlobTool = {
|
|
1958
|
+
name: "glob",
|
|
1959
|
+
description: "Find files matching a glob pattern (e.g., **/*.ts, src/**/*.tsx)",
|
|
1960
|
+
inputSchema: {
|
|
1961
|
+
pattern: {
|
|
1962
|
+
type: "string",
|
|
1963
|
+
description: "Glob pattern like **/*.ts or src/**/*.tsx"
|
|
1964
|
+
},
|
|
1965
|
+
path: {
|
|
1966
|
+
type: "string",
|
|
1967
|
+
description: "Base directory (defaults to current directory)"
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
requiredParams: ["pattern"],
|
|
1971
|
+
async execute(args) {
|
|
1972
|
+
const pattern = args.pattern;
|
|
1973
|
+
const basePath = args.path || ".";
|
|
1974
|
+
if (!pattern) {
|
|
1975
|
+
return { success: false, error: "No pattern provided" };
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
const cfg = await config.load();
|
|
1979
|
+
const results = [];
|
|
1980
|
+
const fullBase = path5.resolve(cfg.workspaceRoot, basePath);
|
|
1981
|
+
await globSearch(fullBase, pattern, results, 100, 0, cfg.workspaceRoot);
|
|
1982
|
+
if (results.length === 0) {
|
|
1983
|
+
return {
|
|
1984
|
+
success: true,
|
|
1985
|
+
output: `No files matching: ${pattern}`
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
return {
|
|
1989
|
+
success: true,
|
|
1990
|
+
output: truncateOutput(`Found ${results.length} files:
|
|
1991
|
+
${results.join("\n")}`)
|
|
1992
|
+
};
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
return {
|
|
1995
|
+
success: false,
|
|
1996
|
+
error: `Glob failed: ${error.message}`
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
async function globSearch(dir, pattern, results, maxResults, depth = 0, workspaceRoot = process.cwd()) {
|
|
2002
|
+
if (results.length >= maxResults || depth > 15) return;
|
|
2003
|
+
try {
|
|
2004
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
2005
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{GLOBSTAR}}/g, ".*");
|
|
2006
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
2007
|
+
for (const entry of entries) {
|
|
2008
|
+
if (results.length >= maxResults) break;
|
|
2009
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
const fullPath = path5.join(dir, entry.name);
|
|
2013
|
+
const relativePath = path5.relative(workspaceRoot, fullPath);
|
|
2014
|
+
if (entry.isDirectory()) {
|
|
2015
|
+
if (pattern.includes("**") || pattern.includes("/")) {
|
|
2016
|
+
await globSearch(fullPath, pattern, results, maxResults, depth + 1, workspaceRoot);
|
|
2017
|
+
}
|
|
2018
|
+
} else if (entry.isFile()) {
|
|
2019
|
+
if (regex.test(relativePath) || regex.test(entry.name)) {
|
|
2020
|
+
results.push(relativePath);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
} catch {
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
var TaskTool = {
|
|
2028
|
+
name: "task",
|
|
2029
|
+
description: "Manage the project plan and todo list. Use this for tracking work items, not for scheduling recurring jobs. actions: create, add_step, complete_step, fail_step, complete_task",
|
|
2030
|
+
inputSchema: {
|
|
2031
|
+
action: {
|
|
2032
|
+
type: "string",
|
|
2033
|
+
description: "Action to perform: create, add_step, complete_step, fail_step, complete_task"
|
|
2034
|
+
},
|
|
2035
|
+
title: {
|
|
2036
|
+
type: "string",
|
|
2037
|
+
description: "Title for new task (required for create)"
|
|
2038
|
+
},
|
|
2039
|
+
step: {
|
|
2040
|
+
type: "string",
|
|
2041
|
+
description: "Step description (required for add_step)"
|
|
2042
|
+
},
|
|
2043
|
+
step_index: {
|
|
2044
|
+
type: "number",
|
|
2045
|
+
description: "Index of step to complete/fail (required for step actions)"
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
2048
|
+
requiredParams: ["action"],
|
|
2049
|
+
async execute(args) {
|
|
2050
|
+
const action = args.action;
|
|
2051
|
+
const title = args.title;
|
|
2052
|
+
const step = args.step;
|
|
2053
|
+
const stepIndex = args.step_index;
|
|
2054
|
+
if (!action) return { success: false, error: "No action provided" };
|
|
2055
|
+
try {
|
|
2056
|
+
switch (action) {
|
|
2057
|
+
case "create":
|
|
2058
|
+
if (!title) return { success: false, error: "Title required for create" };
|
|
2059
|
+
await tasks.create(title);
|
|
2060
|
+
return { success: true, output: `Created task: ${title}` };
|
|
2061
|
+
case "add_step":
|
|
2062
|
+
if (!step) return { success: false, error: "Step text required" };
|
|
2063
|
+
await tasks.addSubtask(step);
|
|
2064
|
+
return { success: true, output: `Added step: ${step}` };
|
|
2065
|
+
case "complete_step":
|
|
2066
|
+
if (stepIndex === void 0) return { success: false, error: "Step index required" };
|
|
2067
|
+
await tasks.completeSubtask(stepIndex);
|
|
2068
|
+
return { success: true, output: `Completed step ${stepIndex}` };
|
|
2069
|
+
case "fail_step":
|
|
2070
|
+
return { success: true, output: `Marked step ${stepIndex} as failed (not implemented in tracker yet)` };
|
|
2071
|
+
case "complete_task":
|
|
2072
|
+
await tasks.complete();
|
|
2073
|
+
return { success: true, output: "Marked task as complete" };
|
|
2074
|
+
default:
|
|
2075
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
2076
|
+
}
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
return { success: false, error: `Task action failed: ${error.message}` };
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
var WebFetchTool = {
|
|
2083
|
+
name: "web_fetch",
|
|
2084
|
+
description: "Fetch content from a URL (for documentation, APIs, etc.)",
|
|
2085
|
+
inputSchema: {
|
|
2086
|
+
url: {
|
|
2087
|
+
type: "string",
|
|
2088
|
+
description: "URL to fetch content from"
|
|
2089
|
+
}
|
|
2090
|
+
},
|
|
2091
|
+
requiredParams: ["url"],
|
|
2092
|
+
async execute(args) {
|
|
2093
|
+
const url = args.url;
|
|
2094
|
+
if (!url) {
|
|
2095
|
+
return { success: false, error: "No URL provided" };
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
new URL(url);
|
|
2099
|
+
} catch {
|
|
2100
|
+
return { success: false, error: "Invalid URL format" };
|
|
2101
|
+
}
|
|
2102
|
+
const blockedDomains = ["localhost", "127.0.0.1", "0.0.0.0", "169.254"];
|
|
2103
|
+
const urlObj = new URL(url);
|
|
2104
|
+
if (blockedDomains.some((d) => urlObj.hostname.includes(d))) {
|
|
2105
|
+
return {
|
|
2106
|
+
success: false,
|
|
2107
|
+
error: "Cannot fetch from local/private addresses"
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
try {
|
|
2111
|
+
const controller = new AbortController();
|
|
2112
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
2113
|
+
const response = await fetch(url, {
|
|
2114
|
+
signal: controller.signal,
|
|
2115
|
+
headers: {
|
|
2116
|
+
"User-Agent": "Obsidian-Next/1.0 (AI Agent CLI)",
|
|
2117
|
+
"Accept": "text/html,application/json,text/plain,*/*"
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
clearTimeout(timeoutId);
|
|
2121
|
+
if (!response.ok) {
|
|
2122
|
+
return {
|
|
2123
|
+
success: false,
|
|
2124
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2128
|
+
let content = await response.text();
|
|
2129
|
+
if (content.length > MAX_OUTPUT_LENGTH) {
|
|
2130
|
+
content = content.slice(0, MAX_OUTPUT_LENGTH) + `
|
|
2131
|
+
|
|
2132
|
+
... [TRUNCATED: ${content.length - MAX_OUTPUT_LENGTH} more characters]`;
|
|
2133
|
+
}
|
|
2134
|
+
if (contentType.includes("text/html")) {
|
|
2135
|
+
content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2136
|
+
}
|
|
2137
|
+
return {
|
|
2138
|
+
success: true,
|
|
2139
|
+
output: truncateOutput(`URL: ${url}
|
|
2140
|
+
Content-Type: ${contentType}
|
|
2141
|
+
${"=".repeat(60)}
|
|
2142
|
+
${content}`)
|
|
2143
|
+
};
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
if (error.name === "AbortError") {
|
|
2146
|
+
return { success: false, error: "Request timed out after 10 seconds" };
|
|
2147
|
+
}
|
|
2148
|
+
return {
|
|
2149
|
+
success: false,
|
|
2150
|
+
error: `Fetch failed: ${error.message}`
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
var MCPManagementTool = {
|
|
2156
|
+
name: "mcp_manage",
|
|
2157
|
+
description: 'Manage MCP servers. actions: add, remove, install. Use "install" to easily add certified tools like "filesystem", "git", "research", or "context7".',
|
|
2158
|
+
inputSchema: {
|
|
2159
|
+
action: {
|
|
2160
|
+
type: "string",
|
|
2161
|
+
description: "Action to perform: add, remove, install"
|
|
2162
|
+
},
|
|
2163
|
+
name: {
|
|
2164
|
+
type: "string",
|
|
2165
|
+
description: 'Name of the server (e.g. "filesystem", "research", "context7")'
|
|
2166
|
+
},
|
|
2167
|
+
command: {
|
|
2168
|
+
type: "string",
|
|
2169
|
+
description: "Command to execute (add only)"
|
|
2170
|
+
},
|
|
2171
|
+
args: {
|
|
2172
|
+
type: "string",
|
|
2173
|
+
description: "Args for command (add only)"
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
requiredParams: ["action", "name"],
|
|
2177
|
+
async execute(args) {
|
|
2178
|
+
const action = args.action;
|
|
2179
|
+
const name = args.name;
|
|
2180
|
+
if (action === "install") {
|
|
2181
|
+
const def = getRegistryDefinition(name);
|
|
2182
|
+
if (!def) {
|
|
2183
|
+
const available = listRegistry().map((r) => r.name).join(", ");
|
|
2184
|
+
return { success: false, error: `Unknown registry item '${name}'. Available: ${available}` };
|
|
2185
|
+
}
|
|
2186
|
+
const existing = mcp.getStatus().find((s) => s.name === name);
|
|
2187
|
+
if (existing && existing.connected) {
|
|
2188
|
+
return { success: true, output: `MCP server '${name}' is already installed and connected.` };
|
|
2189
|
+
}
|
|
2190
|
+
try {
|
|
2191
|
+
await mcp.addServer(name, {
|
|
2192
|
+
command: def.command,
|
|
2193
|
+
args: def.args,
|
|
2194
|
+
autoConnect: false,
|
|
2195
|
+
env: def.env
|
|
2196
|
+
});
|
|
2197
|
+
return { success: true, output: `Successfully installed and connected to certified MCP server '${name}' (${def.description})` };
|
|
2198
|
+
} catch (e) {
|
|
2199
|
+
return { success: false, error: `Failed to install server: ${e.message}` };
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (action === "add") {
|
|
2203
|
+
if (!args.command) return { success: false, error: 'Command is required for "add" action' };
|
|
2204
|
+
const command = args.command;
|
|
2205
|
+
const commandArgs = (args.args || "").split(" ").filter((a) => a.length > 0);
|
|
2206
|
+
try {
|
|
2207
|
+
await mcp.addServer(name, {
|
|
2208
|
+
command,
|
|
2209
|
+
args: commandArgs,
|
|
2210
|
+
autoConnect: false
|
|
2211
|
+
});
|
|
2212
|
+
return { success: true, output: `Successfully added and connected to MCP server '${name}'` };
|
|
2213
|
+
} catch (e) {
|
|
2214
|
+
return { success: false, error: `Failed to add server: ${e.message}` };
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (action === "remove") {
|
|
2218
|
+
try {
|
|
2219
|
+
await mcp.removeServer(name);
|
|
2220
|
+
return { success: true, output: `Successfully removed MCP server '${name}'` };
|
|
2221
|
+
} catch (e) {
|
|
2222
|
+
return { success: false, error: `Failed to remove server: ${e.message}` };
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (action === "connect") {
|
|
2226
|
+
try {
|
|
2227
|
+
const status = mcp.getStatus().find((s) => s.name === name);
|
|
2228
|
+
if (!status) return { success: false, error: `Server '${name}' not found in config` };
|
|
2229
|
+
if (status.connected) return { success: true, output: `Server '${name}' is already connected` };
|
|
2230
|
+
await mcp.connect(name, status.config);
|
|
2231
|
+
return { success: true, output: `Successfully connected to MCP server '${name}'` };
|
|
2232
|
+
} catch (e) {
|
|
2233
|
+
return { success: false, error: `Failed to connect: ${e.message}` };
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (action === "disconnect") {
|
|
2237
|
+
try {
|
|
2238
|
+
await mcp.disconnect(name);
|
|
2239
|
+
return { success: true, output: `Successfully disconnected MCP server '${name}'` };
|
|
2240
|
+
} catch (e) {
|
|
2241
|
+
return { success: false, error: `Failed to disconnect: ${e.message}` };
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (action === "status") {
|
|
2245
|
+
const status = mcp.getStatus().find((s) => s.name === name);
|
|
2246
|
+
if (!status) return { success: false, error: `Server '${name}' not found` };
|
|
2247
|
+
return {
|
|
2248
|
+
success: true,
|
|
2249
|
+
output: `Server: ${name}
|
|
2250
|
+
Status: ${status.connected ? "Connected" : "Disconnected"}
|
|
2251
|
+
Tools: ${status.capabilities ? "Available" : "N/A"}`
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
var MemoryTool = {
|
|
2258
|
+
name: "memory",
|
|
2259
|
+
description: "Store and recall long-term memories: user preferences, project facts, and learned patterns. Use this to remember user information across sessions.",
|
|
2260
|
+
inputSchema: {
|
|
2261
|
+
action: { type: "string", description: "Action: store, recall, search, list, forget" },
|
|
2262
|
+
type: { type: "string", description: "Memory type: user_preference, project_fact, decision_log, learned_pattern (for store)" },
|
|
2263
|
+
key: { type: "string", description: "Unique key for the memory (for store, recall, forget)" },
|
|
2264
|
+
content: { type: "string", description: "Content to store (for store action)" },
|
|
2265
|
+
query: { type: "string", description: "Search query (for search action)" }
|
|
2266
|
+
},
|
|
2267
|
+
requiredParams: ["action"],
|
|
2268
|
+
async execute(args) {
|
|
2269
|
+
const { memory } = await import("./memory-MV3S7GFY.js");
|
|
2270
|
+
const { action, type, key, content, query } = args;
|
|
2271
|
+
await memory.init();
|
|
2272
|
+
if (action === "store") {
|
|
2273
|
+
if (!key || !content) {
|
|
2274
|
+
return { success: false, error: "store requires key and content" };
|
|
2275
|
+
}
|
|
2276
|
+
const memoType = type || "user_preference";
|
|
2277
|
+
const success = await memory.store(memoType, key, content);
|
|
2278
|
+
if (success) {
|
|
2279
|
+
return { success: true, output: `Stored memory: ${key}` };
|
|
2280
|
+
}
|
|
2281
|
+
return { success: false, error: "Failed to store memory" };
|
|
2282
|
+
}
|
|
2283
|
+
if (action === "recall") {
|
|
2284
|
+
if (!key) {
|
|
2285
|
+
return { success: false, error: "recall requires key" };
|
|
2286
|
+
}
|
|
2287
|
+
const memo = await memory.recall(key);
|
|
2288
|
+
if (memo) {
|
|
2289
|
+
return { success: true, output: `[${memo.type}] ${memo.key}: ${memo.content}` };
|
|
2290
|
+
}
|
|
2291
|
+
return { success: true, output: `No memory found for key: ${key}` };
|
|
2292
|
+
}
|
|
2293
|
+
if (action === "search") {
|
|
2294
|
+
if (!query) {
|
|
2295
|
+
return { success: false, error: "search requires query" };
|
|
2296
|
+
}
|
|
2297
|
+
const memos = await memory.search(query, type);
|
|
2298
|
+
if (memos.length === 0) {
|
|
2299
|
+
return { success: true, output: "No memories found matching query" };
|
|
2300
|
+
}
|
|
2301
|
+
const lines = memos.map((m) => `- [${m.type}] ${m.key}: ${m.content}`);
|
|
2302
|
+
return { success: true, output: `Found ${memos.length} memories:
|
|
2303
|
+
${lines.join("\n")}` };
|
|
2304
|
+
}
|
|
2305
|
+
if (action === "list") {
|
|
2306
|
+
const memoType = type;
|
|
2307
|
+
let memos;
|
|
2308
|
+
if (memoType) {
|
|
2309
|
+
memos = await memory.getByType(memoType);
|
|
2310
|
+
if (memos.length === 0) {
|
|
2311
|
+
return { success: true, output: `No ${memoType} memories found` };
|
|
2312
|
+
}
|
|
2313
|
+
const lines = memos.map((m) => `- ${m.key}: ${m.content}`);
|
|
2314
|
+
return { success: true, output: `${memoType} memories:
|
|
2315
|
+
${lines.join("\n")}` };
|
|
2316
|
+
} else {
|
|
2317
|
+
const stats = await memory.getStats();
|
|
2318
|
+
const allMemos = [];
|
|
2319
|
+
for (const t of Object.keys(stats.byType)) {
|
|
2320
|
+
const typeMemos = await memory.getByType(t);
|
|
2321
|
+
if (typeMemos.length > 0) {
|
|
2322
|
+
allMemos.push(`--- ${t} ---`);
|
|
2323
|
+
allMemos.push(...typeMemos.map((m) => `- ${m.key}: ${m.content}`));
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (allMemos.length === 0) return { success: true, output: "No memories found in any category." };
|
|
2327
|
+
return { success: true, output: `Current Memory Bank:
|
|
2328
|
+
${allMemos.join("\n")}` };
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (action === "forget") {
|
|
2332
|
+
if (!key) {
|
|
2333
|
+
return { success: false, error: "forget requires key" };
|
|
2334
|
+
}
|
|
2335
|
+
const success = await memory.forget(key);
|
|
2336
|
+
if (success) {
|
|
2337
|
+
return { success: true, output: `Forgot memory: ${key}` };
|
|
2338
|
+
}
|
|
2339
|
+
return { success: false, error: "Failed to forget memory" };
|
|
2340
|
+
}
|
|
2341
|
+
return { success: false, error: `Unknown action: ${action}. Valid: store, recall, search, list, forget` };
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
var UnscheduleTool = {
|
|
2345
|
+
name: "unschedule_task",
|
|
2346
|
+
description: "Unschedule a previously scheduled background cron job. Requires the task ID.",
|
|
2347
|
+
inputSchema: {
|
|
2348
|
+
taskId: {
|
|
2349
|
+
type: "string",
|
|
2350
|
+
description: "The ID of the task to unschedule (obtained from list_scheduled_tasks)."
|
|
2351
|
+
}
|
|
2352
|
+
},
|
|
2353
|
+
requiredParams: ["taskId"],
|
|
2354
|
+
async execute(args) {
|
|
2355
|
+
const taskId = args.taskId;
|
|
2356
|
+
if (!taskId) {
|
|
2357
|
+
return { success: false, error: "Task ID is required to unschedule a task." };
|
|
2358
|
+
}
|
|
2359
|
+
try {
|
|
2360
|
+
const success = await scheduler.removeTask(taskId);
|
|
2361
|
+
if (success) {
|
|
2362
|
+
return { success: true, output: `Successfully unscheduled task: ${taskId}` };
|
|
2363
|
+
} else {
|
|
2364
|
+
return { success: false, output: `Failed to unschedule task: ${taskId}. Task not found or already inactive.` };
|
|
2365
|
+
}
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
return { success: false, error: `Failed to unschedule task: ${error.message}` };
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
var ComputerUseTool = {
|
|
2372
|
+
name: "computer",
|
|
2373
|
+
description: `Desktop interaction. PREFER BASH for URLs/apps (e.g., bash 'open "https://youtube.com"').
|
|
2374
|
+
|
|
2375
|
+
SMART ACTIONS (use first):
|
|
2376
|
+
- find_and_click: Click by label ("Submit", "Play") - no coordinates needed
|
|
2377
|
+
- get_ui_context: List buttons/fields in current window
|
|
2378
|
+
- get_buttons: List all button labels
|
|
2379
|
+
- activate_app: Bring app to front
|
|
2380
|
+
|
|
2381
|
+
COORDINATE ACTIONS (use when smart actions fail):
|
|
2382
|
+
- screenshot: Capture screen
|
|
2383
|
+
- left_click: Click at [x,y]
|
|
2384
|
+
- type: Type text
|
|
2385
|
+
- key: Press key (cmd+l, Return, etc.)
|
|
2386
|
+
- scroll: Scroll at position`,
|
|
2387
|
+
inputSchema: {
|
|
2388
|
+
action: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
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."
|
|
2391
|
+
},
|
|
2392
|
+
coordinate: {
|
|
2393
|
+
type: "array",
|
|
2394
|
+
items: { type: "number" },
|
|
2395
|
+
description: "[x, y] pixel coordinates for coordinate-based actions."
|
|
2396
|
+
},
|
|
2397
|
+
text: {
|
|
2398
|
+
type: "string",
|
|
2399
|
+
description: "Text to type, or modifier key for clicks (shift, control, alt, command)."
|
|
2400
|
+
},
|
|
2401
|
+
key: {
|
|
2402
|
+
type: "string",
|
|
2403
|
+
description: "Key to press (e.g., enter, escape, ctrl+c)."
|
|
2404
|
+
},
|
|
2405
|
+
label: {
|
|
2406
|
+
type: "string",
|
|
2407
|
+
description: 'UI element label for find_and_click (e.g., "Submit", "Cancel", "OK").'
|
|
2408
|
+
},
|
|
2409
|
+
app_name: {
|
|
2410
|
+
type: "string",
|
|
2411
|
+
description: "Application name for activate_app or to scope element search."
|
|
2412
|
+
},
|
|
2413
|
+
scroll_direction: {
|
|
2414
|
+
type: "string",
|
|
2415
|
+
description: "up, down, left, or right"
|
|
2416
|
+
},
|
|
2417
|
+
scroll_amount: {
|
|
2418
|
+
type: "number",
|
|
2419
|
+
description: "Amount to scroll (default: 3)"
|
|
2420
|
+
},
|
|
2421
|
+
start_coordinate: {
|
|
2422
|
+
type: "array",
|
|
2423
|
+
items: { type: "number" },
|
|
2424
|
+
description: "[x, y] start coordinates for drag"
|
|
2425
|
+
},
|
|
2426
|
+
end_coordinate: {
|
|
2427
|
+
type: "array",
|
|
2428
|
+
items: { type: "number" },
|
|
2429
|
+
description: "[x, y] end coordinates for drag"
|
|
2430
|
+
},
|
|
2431
|
+
duration: {
|
|
2432
|
+
type: "number",
|
|
2433
|
+
description: "Duration in milliseconds (for wait, hold_key)"
|
|
2434
|
+
},
|
|
2435
|
+
region: {
|
|
2436
|
+
type: "array",
|
|
2437
|
+
items: { type: "number" },
|
|
2438
|
+
description: "[x1, y1, x2, y2] region for zoom"
|
|
2439
|
+
}
|
|
2440
|
+
},
|
|
2441
|
+
requiredParams: ["action"],
|
|
2442
|
+
async execute(args) {
|
|
2443
|
+
const actionType = args.action;
|
|
2444
|
+
if (!actionType) {
|
|
2445
|
+
return { success: false, error: "No computer action provided. Use: screenshot, left_click, type, key, mouse_move, scroll, etc." };
|
|
2446
|
+
}
|
|
2447
|
+
const validateCoord = (coord, name = "coordinate") => {
|
|
2448
|
+
if (!coord || !Array.isArray(coord) || coord.length < 2) {
|
|
2449
|
+
return null;
|
|
2450
|
+
}
|
|
2451
|
+
const x = Number(coord[0]);
|
|
2452
|
+
const y = Number(coord[1]);
|
|
2453
|
+
if (isNaN(x) || isNaN(y)) return null;
|
|
2454
|
+
return [x, y];
|
|
2455
|
+
};
|
|
2456
|
+
const scaleToNative = (coord) => {
|
|
2457
|
+
if (lastScreenshotScale >= 1) return coord;
|
|
2458
|
+
return [
|
|
2459
|
+
Math.round(coord[0] / lastScreenshotScale),
|
|
2460
|
+
Math.round(coord[1] / lastScreenshotScale)
|
|
2461
|
+
];
|
|
2462
|
+
};
|
|
2463
|
+
try {
|
|
2464
|
+
let output;
|
|
2465
|
+
switch (actionType) {
|
|
2466
|
+
case "screenshot":
|
|
2467
|
+
const screenshotResult = await takeScreenshotForAPI(false);
|
|
2468
|
+
lastScreenshotScale = screenshotResult.scale;
|
|
2469
|
+
bus.emitAgent({
|
|
2470
|
+
type: "computer_scale_update",
|
|
2471
|
+
scale: screenshotResult.scale,
|
|
2472
|
+
scaledWidth: screenshotResult.width,
|
|
2473
|
+
scaledHeight: screenshotResult.height,
|
|
2474
|
+
nativeWidth: Math.round(screenshotResult.width / screenshotResult.scale),
|
|
2475
|
+
nativeHeight: Math.round(screenshotResult.height / screenshotResult.scale)
|
|
2476
|
+
});
|
|
2477
|
+
return {
|
|
2478
|
+
success: true,
|
|
2479
|
+
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)}`,
|
|
2480
|
+
content: [{ type: "image", data: screenshotResult.base64, mimeType: "image/png" }]
|
|
2481
|
+
};
|
|
2482
|
+
case "left_click": {
|
|
2483
|
+
const coord = validateCoord(args.coordinate);
|
|
2484
|
+
if (!coord) return { success: false, error: "left_click requires coordinate: [x, y] as numbers" };
|
|
2485
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2486
|
+
await computer.leftClick(nativeX, nativeY, args.text);
|
|
2487
|
+
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.`;
|
|
2488
|
+
break;
|
|
2489
|
+
}
|
|
2490
|
+
case "type":
|
|
2491
|
+
if (!args.text && args.text !== "") return { success: false, error: "type requires text parameter" };
|
|
2492
|
+
await computer.typeText(args.text);
|
|
2493
|
+
output = `Typed: "${args.text.substring(0, 30)}${args.text.length > 30 ? "..." : ""}"`;
|
|
2494
|
+
break;
|
|
2495
|
+
case "key":
|
|
2496
|
+
const keyToPress = args.key || args.text;
|
|
2497
|
+
if (!keyToPress) return { success: false, error: 'key requires key parameter (e.g., "enter", "escape", "ctrl+c")' };
|
|
2498
|
+
await computer.pressKey(keyToPress);
|
|
2499
|
+
output = `Pressed key: ${keyToPress}`;
|
|
2500
|
+
break;
|
|
2501
|
+
case "mouse_move": {
|
|
2502
|
+
const coord = validateCoord(args.coordinate);
|
|
2503
|
+
if (!coord) return { success: false, error: "mouse_move requires coordinate: [x, y]" };
|
|
2504
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2505
|
+
await computer.mouseMove(nativeX, nativeY);
|
|
2506
|
+
output = `Mouse moved to screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
2509
|
+
case "scroll": {
|
|
2510
|
+
const coord = validateCoord(args.coordinate);
|
|
2511
|
+
if (!coord) return { success: false, error: "scroll requires coordinate: [x, y]" };
|
|
2512
|
+
if (!args.scroll_direction) return { success: false, error: "scroll requires scroll_direction: up|down|left|right" };
|
|
2513
|
+
const amount = args.scroll_amount || 3;
|
|
2514
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2515
|
+
await computer.scroll(nativeX, nativeY, args.scroll_direction, amount, args.text);
|
|
2516
|
+
output = `Scrolled ${args.scroll_direction} by ${amount} at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2517
|
+
break;
|
|
2518
|
+
}
|
|
2519
|
+
case "left_click_drag": {
|
|
2520
|
+
const startCoord = validateCoord(args.start_coordinate, "start_coordinate");
|
|
2521
|
+
const endCoord = validateCoord(args.end_coordinate, "end_coordinate");
|
|
2522
|
+
if (!startCoord) return { success: false, error: "left_click_drag requires start_coordinate: [x, y]" };
|
|
2523
|
+
if (!endCoord) return { success: false, error: "left_click_drag requires end_coordinate: [x, y]" };
|
|
2524
|
+
const [nativeStartX, nativeStartY] = scaleToNative(startCoord);
|
|
2525
|
+
const [nativeEndX, nativeEndY] = scaleToNative(endCoord);
|
|
2526
|
+
await computer.leftClickDrag(nativeStartX, nativeStartY, nativeEndX, nativeEndY);
|
|
2527
|
+
output = `Dragged from screenshot (${startCoord[0]}, ${startCoord[1]}) -> native (${nativeStartX}, ${nativeStartY}) to screenshot (${endCoord[0]}, ${endCoord[1]}) -> native (${nativeEndX}, ${nativeEndY}).`;
|
|
2528
|
+
break;
|
|
2529
|
+
}
|
|
2530
|
+
case "right_click": {
|
|
2531
|
+
const coord = validateCoord(args.coordinate);
|
|
2532
|
+
if (!coord) return { success: false, error: "right_click requires coordinate: [x, y]" };
|
|
2533
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2534
|
+
await computer.rightClick(nativeX, nativeY, args.text);
|
|
2535
|
+
output = `Right click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2538
|
+
case "middle_click": {
|
|
2539
|
+
const coord = validateCoord(args.coordinate);
|
|
2540
|
+
if (!coord) return { success: false, error: "middle_click requires coordinate: [x, y]" };
|
|
2541
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2542
|
+
await computer.middleClick(nativeX, nativeY, args.text);
|
|
2543
|
+
output = `Middle click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2544
|
+
break;
|
|
2545
|
+
}
|
|
2546
|
+
case "double_click": {
|
|
2547
|
+
const coord = validateCoord(args.coordinate);
|
|
2548
|
+
if (!coord) return { success: false, error: "double_click requires coordinate: [x, y]" };
|
|
2549
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2550
|
+
await computer.doubleClick(nativeX, nativeY, args.text);
|
|
2551
|
+
output = `Double click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2552
|
+
break;
|
|
2553
|
+
}
|
|
2554
|
+
case "triple_click": {
|
|
2555
|
+
const coord = validateCoord(args.coordinate);
|
|
2556
|
+
if (!coord) return { success: false, error: "triple_click requires coordinate: [x, y]" };
|
|
2557
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2558
|
+
await computer.tripleClick(nativeX, nativeY, args.text);
|
|
2559
|
+
output = `Triple click at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2560
|
+
break;
|
|
2561
|
+
}
|
|
2562
|
+
case "left_mouse_down": {
|
|
2563
|
+
const coord = validateCoord(args.coordinate);
|
|
2564
|
+
if (!coord) return { success: false, error: "left_mouse_down requires coordinate: [x, y]" };
|
|
2565
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2566
|
+
await computer.leftMouseDown(nativeX, nativeY);
|
|
2567
|
+
output = `Mouse down at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2568
|
+
break;
|
|
2569
|
+
}
|
|
2570
|
+
case "left_mouse_up": {
|
|
2571
|
+
const coord = validateCoord(args.coordinate);
|
|
2572
|
+
if (!coord) return { success: false, error: "left_mouse_up requires coordinate: [x, y]" };
|
|
2573
|
+
const [nativeX, nativeY] = scaleToNative(coord);
|
|
2574
|
+
await computer.leftMouseUp(nativeX, nativeY);
|
|
2575
|
+
output = `Mouse up at screenshot (${coord[0]}, ${coord[1]}) -> native (${nativeX}, ${nativeY}).`;
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2578
|
+
case "hold_key": {
|
|
2579
|
+
const keyToHold = args.key || args.text;
|
|
2580
|
+
if (!keyToHold) return { success: false, error: "hold_key requires key parameter" };
|
|
2581
|
+
const duration = args.duration || 1;
|
|
2582
|
+
await computer.holdKey(keyToHold, duration);
|
|
2583
|
+
output = `Held ${keyToHold} for ${duration}s.`;
|
|
2584
|
+
break;
|
|
2585
|
+
}
|
|
2586
|
+
case "wait": {
|
|
2587
|
+
const duration = args.duration || 1e3;
|
|
2588
|
+
await computer.wait(duration);
|
|
2589
|
+
output = `Waited ${duration}ms.`;
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
case "zoom": {
|
|
2593
|
+
if (!args.region || !Array.isArray(args.region) || args.region.length < 4) {
|
|
2594
|
+
return { success: false, error: "zoom requires region: [x1, y1, x2, y2]" };
|
|
2595
|
+
}
|
|
2596
|
+
const [x1, y1, x2, y2] = args.region;
|
|
2597
|
+
const [nativeX1, nativeY1] = scaleToNative([x1, y1]);
|
|
2598
|
+
const [nativeX2, nativeY2] = scaleToNative([x2, y2]);
|
|
2599
|
+
const zoomResult = await computer.zoom(nativeX1, nativeY1, nativeX2, nativeY2);
|
|
2600
|
+
return {
|
|
2601
|
+
success: true,
|
|
2602
|
+
output: `Zoomed region screenshot (${x1}, ${y1}) to (${x2}, ${y2}) -> native (${nativeX1}, ${nativeY1}) to (${nativeX2}, ${nativeY2}).`,
|
|
2603
|
+
content: [{ type: "image", data: zoomResult, mimeType: "image/png" }]
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
case "get_dimensions":
|
|
2607
|
+
const dims = await computer.getDisplayDimensions();
|
|
2608
|
+
return { success: true, output: `Display: ${dims.width}x${dims.height}. Use these dimensions for coordinate calculations.` };
|
|
2609
|
+
case "batch":
|
|
2610
|
+
if (!args.commands || !Array.isArray(args.commands)) {
|
|
2611
|
+
return { success: false, error: "batch requires commands: string[]" };
|
|
2612
|
+
}
|
|
2613
|
+
await computer.executeBatch(args.commands);
|
|
2614
|
+
output = `Batch executed ${args.commands.length} commands.`;
|
|
2615
|
+
break;
|
|
2616
|
+
// ==================== SMART ACCESSIBILITY-BASED ACTIONS ====================
|
|
2617
|
+
case "find_and_click": {
|
|
2618
|
+
if (!args.label) return { success: false, error: 'find_and_click requires label parameter (e.g., "Submit", "OK")' };
|
|
2619
|
+
const clicked = await clickElementByLabel(args.label, args.app_name);
|
|
2620
|
+
if (clicked) {
|
|
2621
|
+
output = `Clicked element labeled "${args.label}" via accessibility API.`;
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
const coords = await findClickableByLabel(args.label);
|
|
2625
|
+
if (coords) {
|
|
2626
|
+
await computer.leftClick(coords[0], coords[1]);
|
|
2627
|
+
output = `Clicked "${args.label}" at (${coords[0]}, ${coords[1]}).`;
|
|
2628
|
+
break;
|
|
2629
|
+
}
|
|
2630
|
+
return { success: false, error: `Could not find element labeled "${args.label}". Try using screenshot + coordinate-based click.` };
|
|
2631
|
+
}
|
|
2632
|
+
case "get_ui_context": {
|
|
2633
|
+
const uiContext = await getUIContext();
|
|
2634
|
+
return {
|
|
2635
|
+
success: true,
|
|
2636
|
+
output: `Current UI State:
|
|
2637
|
+
${uiContext}
|
|
2638
|
+
|
|
2639
|
+
Use find_and_click with a button label, or screenshot + coordinates for unlisted elements.`
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
case "get_buttons": {
|
|
2643
|
+
const buttons = await getButtons(args.app_name);
|
|
2644
|
+
if (buttons.length === 0) {
|
|
2645
|
+
return { success: true, output: "No buttons found in current window. Try screenshot to see the UI." };
|
|
2646
|
+
}
|
|
2647
|
+
return {
|
|
2648
|
+
success: true,
|
|
2649
|
+
output: `Available buttons (${buttons.length}):
|
|
2650
|
+
${buttons.map((b) => ` - "${b}"`).join("\n")}
|
|
2651
|
+
|
|
2652
|
+
Use find_and_click with any of these labels.`
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
case "activate_app": {
|
|
2656
|
+
if (!args.app_name) return { success: false, error: "activate_app requires app_name parameter" };
|
|
2657
|
+
const activated = await activateApp(args.app_name);
|
|
2658
|
+
if (activated) {
|
|
2659
|
+
output = `Activated "${args.app_name}".`;
|
|
2660
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2661
|
+
const verifyScreenshot = await takeScreenshotForAPI(false);
|
|
2662
|
+
return {
|
|
2663
|
+
success: true,
|
|
2664
|
+
output: `${output} Window now in focus.`,
|
|
2665
|
+
content: [{ type: "image", data: verifyScreenshot.base64, mimeType: "image/png" }]
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
return { success: false, error: `Could not activate "${args.app_name}". Check if the app is running.` };
|
|
2669
|
+
}
|
|
2670
|
+
case "get_focused_app": {
|
|
2671
|
+
const app = await getFocusedApp();
|
|
2672
|
+
return { success: true, output: `Currently focused: ${app}` };
|
|
2673
|
+
}
|
|
2674
|
+
default:
|
|
2675
|
+
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.` };
|
|
2676
|
+
}
|
|
2677
|
+
const interactionActions = ["left_click", "right_click", "double_click", "triple_click", "type", "key", "left_click_drag", "batch", "find_and_click"];
|
|
2678
|
+
if (interactionActions.includes(actionType)) {
|
|
2679
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2680
|
+
const verifyScreenshot = await takeScreenshotForAPI(true);
|
|
2681
|
+
return {
|
|
2682
|
+
success: true,
|
|
2683
|
+
output: `${output || "Action executed."} Verification captured (${verifyScreenshot.width}x${verifyScreenshot.height}).`,
|
|
2684
|
+
content: [{ type: "image", data: verifyScreenshot.base64, mimeType: "image/png" }]
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
return { success: true, output: output || "Computer action executed successfully." };
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
return { success: false, error: `Computer action failed: ${error.message}` };
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
var CreateSkillTool = {
|
|
2694
|
+
name: "create_skill",
|
|
2695
|
+
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.",
|
|
2696
|
+
inputSchema: {
|
|
2697
|
+
name: {
|
|
2698
|
+
type: "string",
|
|
2699
|
+
description: 'Name of the tool (e.g., "jira_issue_create")'
|
|
2700
|
+
},
|
|
2701
|
+
description: {
|
|
2702
|
+
type: "string",
|
|
2703
|
+
description: "What the tool does"
|
|
2704
|
+
},
|
|
2705
|
+
code: {
|
|
2706
|
+
type: "string",
|
|
2707
|
+
description: "Node.js code for the tool. Must export default a Tool object."
|
|
2708
|
+
}
|
|
2709
|
+
},
|
|
2710
|
+
requiredParams: ["name", "description", "code"],
|
|
2711
|
+
async execute(args) {
|
|
2712
|
+
const name = args.name;
|
|
2713
|
+
const code = args.code;
|
|
2714
|
+
const skillsDir = path5.join(os4.homedir(), ".obsidian-next", "skills");
|
|
2715
|
+
const skillPath = path5.join(skillsDir, `${name}.js`);
|
|
2716
|
+
try {
|
|
2717
|
+
if (!fsSync.existsSync(skillsDir)) {
|
|
2718
|
+
fsSync.mkdirSync(skillsDir, { recursive: true });
|
|
2719
|
+
}
|
|
2720
|
+
await fs5.writeFile(skillPath, code, "utf-8");
|
|
2721
|
+
const module = await import(`file://${skillPath}?t=${Date.now()}`);
|
|
2722
|
+
if (module.default && module.default.name) {
|
|
2723
|
+
tools.register(module.default);
|
|
2724
|
+
return {
|
|
2725
|
+
success: true,
|
|
2726
|
+
output: `Skill '${name}' created and registered successfully. It is now available for use.`
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
return { success: false, error: "Skill code must export default a Tool object." };
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
return {
|
|
2732
|
+
success: false,
|
|
2733
|
+
error: `Failed to create skill: ${error.message}`
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
var ToolRegistry = class {
|
|
2739
|
+
tools = /* @__PURE__ */ new Map();
|
|
2740
|
+
skillsDir = path5.join(os4.homedir(), ".obsidian-next", "skills");
|
|
2741
|
+
constructor() {
|
|
2742
|
+
this.register(BashTool);
|
|
2743
|
+
this.register(ReadTool);
|
|
2744
|
+
this.register(WriteTool);
|
|
2745
|
+
this.register(EditTool);
|
|
2746
|
+
this.register(ListTool);
|
|
2747
|
+
this.register(GrepTool);
|
|
2748
|
+
this.register(GlobTool);
|
|
2749
|
+
this.register(TaskTool);
|
|
2750
|
+
this.register(WebFetchTool);
|
|
2751
|
+
this.register(MCPManagementTool);
|
|
2752
|
+
this.register(ScheduleTool);
|
|
2753
|
+
this.register(ListScheduledTasksTool);
|
|
2754
|
+
this.register(UnscheduleTool);
|
|
2755
|
+
this.register(MemoryTool);
|
|
2756
|
+
this.register(ComputerUseTool);
|
|
2757
|
+
this.register(CreateSkillTool);
|
|
2758
|
+
}
|
|
2759
|
+
async init() {
|
|
2760
|
+
await this.loadSkills();
|
|
2761
|
+
}
|
|
2762
|
+
async loadSkills() {
|
|
2763
|
+
if (!fsSync.existsSync(this.skillsDir)) {
|
|
2764
|
+
fsSync.mkdirSync(this.skillsDir, { recursive: true });
|
|
2765
|
+
}
|
|
2766
|
+
try {
|
|
2767
|
+
const files = await fs5.readdir(this.skillsDir);
|
|
2768
|
+
for (const file of files) {
|
|
2769
|
+
if (file.endsWith(".js")) {
|
|
2770
|
+
try {
|
|
2771
|
+
const skillPath = path5.join(this.skillsDir, file);
|
|
2772
|
+
const module = await import(`file://${skillPath}`);
|
|
2773
|
+
if (module.default && module.default.name) {
|
|
2774
|
+
this.register(module.default);
|
|
2775
|
+
}
|
|
2776
|
+
} catch (e) {
|
|
2777
|
+
console.error(`Failed to load skill ${file}:`, e);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
} catch (e) {
|
|
2782
|
+
console.error("Failed to read skills directory:", e);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
register(tool) {
|
|
2786
|
+
this.tools.set(tool.name, tool);
|
|
2787
|
+
}
|
|
2788
|
+
has(name) {
|
|
2789
|
+
return this.tools.has(name);
|
|
2790
|
+
}
|
|
2791
|
+
get(name) {
|
|
2792
|
+
return this.tools.get(name);
|
|
2793
|
+
}
|
|
2794
|
+
async list() {
|
|
2795
|
+
const staticTools = Array.from(this.tools.values());
|
|
2796
|
+
try {
|
|
2797
|
+
const dynamicTools = await mcp.listTools();
|
|
2798
|
+
const mcpAdapters = dynamicTools.map((dt) => ({
|
|
2799
|
+
name: `${dt.server}_${dt.name}`,
|
|
2800
|
+
// Namespace: server_toolname
|
|
2801
|
+
description: `[MCP: ${dt.server}] ${dt.description || ""}`,
|
|
2802
|
+
inputSchema: dt.inputSchema?.properties || {},
|
|
2803
|
+
requiredParams: dt.inputSchema?.required || [],
|
|
2804
|
+
execute: async (args) => {
|
|
2805
|
+
const result = await mcp.callTool(dt.server, dt.name, args);
|
|
2806
|
+
if (result.isError) {
|
|
2807
|
+
return { success: false, error: "MCP Tool Error" };
|
|
2808
|
+
}
|
|
2809
|
+
const output = result.content.filter((c) => c.text).map((c) => c.text).join("\n");
|
|
2810
|
+
return { success: !result.isError, output, content: result.content };
|
|
2811
|
+
}
|
|
2812
|
+
}));
|
|
2813
|
+
return [...staticTools, ...mcpAdapters];
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
console.error("Failed to list MCP tools:", error);
|
|
2816
|
+
return staticTools;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
async execute(name, args) {
|
|
2820
|
+
let tool = this.tools.get(name);
|
|
2821
|
+
if (!tool) {
|
|
2822
|
+
const mcpTools = await this.list();
|
|
2823
|
+
tool = mcpTools.find((t) => t.name === name);
|
|
2824
|
+
}
|
|
2825
|
+
if (!tool) {
|
|
2826
|
+
return {
|
|
2827
|
+
success: false,
|
|
2828
|
+
error: `Unknown tool: ${name}. Available: ${(await this.list()).map((t) => t.name).join(", ")}`
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
bus.emitAgent({
|
|
2832
|
+
type: "tool_start",
|
|
2833
|
+
tool: name,
|
|
2834
|
+
args: JSON.stringify(args, null, 2)
|
|
2835
|
+
});
|
|
2836
|
+
const result = await tool.execute(args);
|
|
2837
|
+
const rawOutput = result.success ? result.output || "Success" : result.error || "Failed";
|
|
2838
|
+
const redacted = redactor.redactToolOutput(name, rawOutput);
|
|
2839
|
+
bus.emitAgent({
|
|
2840
|
+
type: "tool_result",
|
|
2841
|
+
tool: name,
|
|
2842
|
+
output: redacted.text,
|
|
2843
|
+
isError: !result.success
|
|
2844
|
+
});
|
|
2845
|
+
return result;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
var ScheduleTool = {
|
|
2849
|
+
name: "schedule_task",
|
|
2850
|
+
description: 'Schedule a recurring background cron job. Use this for requests like "every hour", "at 9am", or "check every X".',
|
|
2851
|
+
inputSchema: {
|
|
2852
|
+
cron: {
|
|
2853
|
+
type: "string",
|
|
2854
|
+
description: 'Cron expression (e.g. "* * * * *")'
|
|
2855
|
+
},
|
|
2856
|
+
ability: {
|
|
2857
|
+
type: "string",
|
|
2858
|
+
description: 'Name of the ability to execute. Available: "system:bash", "system:echo", "system:notify" (sound + alert), "system:summary", "system:heartbeat"'
|
|
2859
|
+
},
|
|
2860
|
+
params: {
|
|
2861
|
+
type: "string",
|
|
2862
|
+
description: 'JSON string of parameters. For system:notify, use {"title": "...", "message": "..."}. For system:bash, use {"command": "..."}'
|
|
2863
|
+
}
|
|
2864
|
+
},
|
|
2865
|
+
requiredParams: ["cron", "ability"],
|
|
2866
|
+
async execute(args) {
|
|
2867
|
+
const cron = args.cron;
|
|
2868
|
+
const ability = args.ability;
|
|
2869
|
+
const paramsStr = args.params || "{}";
|
|
2870
|
+
if (!cron || !ability) {
|
|
2871
|
+
return { success: false, error: "Cron expression and ability name are required" };
|
|
2872
|
+
}
|
|
2873
|
+
let params = {};
|
|
2874
|
+
try {
|
|
2875
|
+
params = JSON.parse(paramsStr);
|
|
2876
|
+
} catch {
|
|
2877
|
+
return { success: false, error: "Invalid JSON parameters" };
|
|
2878
|
+
}
|
|
2879
|
+
try {
|
|
2880
|
+
const task = await scheduler.scheduleTask(cron, ability, params);
|
|
2881
|
+
return {
|
|
2882
|
+
success: true,
|
|
2883
|
+
output: `Scheduled task ${task.id}: ${ability} @ "${cron}"`
|
|
2884
|
+
};
|
|
2885
|
+
} catch (error) {
|
|
2886
|
+
return {
|
|
2887
|
+
success: false,
|
|
2888
|
+
error: `Failed to schedule task: ${error.message}`
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
var ListScheduledTasksTool = {
|
|
2894
|
+
name: "list_scheduled_tasks",
|
|
2895
|
+
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.',
|
|
2896
|
+
inputSchema: {},
|
|
2897
|
+
requiredParams: [],
|
|
2898
|
+
async execute(args) {
|
|
2899
|
+
try {
|
|
2900
|
+
const tasks2 = scheduler.listTasks();
|
|
2901
|
+
if (tasks2.length === 0) {
|
|
2902
|
+
return { success: true, output: "No active scheduled tasks." };
|
|
2903
|
+
}
|
|
2904
|
+
const header = `ID | CRON | COMMAND | LAST RUN | NEXT RUN
|
|
2905
|
+
${"-".repeat(80)}`;
|
|
2906
|
+
const rows = tasks2.map((t) => {
|
|
2907
|
+
const last = t.last_run_at ? new Date(t.last_run_at).toLocaleString() : "Never";
|
|
2908
|
+
const next = t.next_run_at ? new Date(t.next_run_at).toLocaleString() : "Unknown";
|
|
2909
|
+
return `${t.id} | ${t.cron_expression} | ${t.command} | ${last} | ${next}`;
|
|
2910
|
+
}).join("\n");
|
|
2911
|
+
return {
|
|
2912
|
+
success: true,
|
|
2913
|
+
output: `${header}
|
|
2914
|
+
${rows}`
|
|
2915
|
+
};
|
|
2916
|
+
} catch (error) {
|
|
2917
|
+
return {
|
|
2918
|
+
success: false,
|
|
2919
|
+
error: `Failed to list tasks: ${error.message}`
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
};
|
|
2924
|
+
var tools = new ToolRegistry();
|
|
2925
|
+
|
|
2926
|
+
// src/core/llm.ts
|
|
2927
|
+
var MAX_TOOL_ITERATIONS = 67;
|
|
2928
|
+
var CONTEXT = {
|
|
2929
|
+
MAX_MESSAGES: 40,
|
|
2930
|
+
KEEP_FIRST: 2,
|
|
2931
|
+
KEEP_LAST: 15,
|
|
2932
|
+
BUFFER: 5,
|
|
2933
|
+
TOKEN_LIMIT_WARN: 0.8,
|
|
2934
|
+
TOKEN_LIMIT_PRUNE: 0.9,
|
|
2935
|
+
TOKEN_LIMIT_STOP: 0.98,
|
|
2936
|
+
MAX_TOKENS_TOTAL: 2e5
|
|
2937
|
+
};
|
|
2938
|
+
var LLMClient = class {
|
|
2939
|
+
client = null;
|
|
2940
|
+
lastConfig = null;
|
|
2941
|
+
conversationHistory = [];
|
|
2942
|
+
toolIterations = 0;
|
|
2943
|
+
accumulatedInputTokens = 0;
|
|
2944
|
+
accumulatedOutputTokens = 0;
|
|
2945
|
+
accumulatedCacheReadTokens = 0;
|
|
2946
|
+
accumulatedCacheCreationTokens = 0;
|
|
2947
|
+
abortController = null;
|
|
2948
|
+
currentInterruptHandler = null;
|
|
2949
|
+
lastToolOutputContent = null;
|
|
2950
|
+
constructor() {
|
|
2951
|
+
bus.on("agent", (event) => {
|
|
2952
|
+
if (event.type === "computer_scale_update" && this.computerUseState.enabled) {
|
|
2953
|
+
this.computerUseState.scale = event.scale;
|
|
2954
|
+
this.computerUseState.scaledWidth = event.scaledWidth;
|
|
2955
|
+
this.computerUseState.scaledHeight = event.scaledHeight;
|
|
2956
|
+
this.computerUseState.displayWidth = event.nativeWidth;
|
|
2957
|
+
this.computerUseState.displayHeight = event.nativeHeight;
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
// Computer Use State - proper Anthropic API integration
|
|
2962
|
+
computerUseState = {
|
|
2963
|
+
enabled: false,
|
|
2964
|
+
displayWidth: 1920,
|
|
2965
|
+
displayHeight: 1080,
|
|
2966
|
+
scale: 1,
|
|
2967
|
+
scaledWidth: 1920,
|
|
2968
|
+
scaledHeight: 1080
|
|
2969
|
+
};
|
|
2970
|
+
async initialize() {
|
|
2971
|
+
const cfg = await config.load();
|
|
2972
|
+
await usage.init();
|
|
2973
|
+
const envFileCheck = await detectEnvFile(cfg.workspaceRoot);
|
|
2974
|
+
if (envFileCheck.found) {
|
|
2975
|
+
bus.emitAgent({
|
|
2976
|
+
type: "thought",
|
|
2977
|
+
content: `[WARN] Found .env file with API key at ${envFileCheck.path}. For security, run /init to migrate to secure storage.`,
|
|
2978
|
+
hidden: false
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
if (config.hasDeprecatedKey()) {
|
|
2982
|
+
const deprecatedKey = config.getDeprecatedApiKey();
|
|
2983
|
+
if (deprecatedKey) {
|
|
2984
|
+
bus.emitAgent({
|
|
2985
|
+
type: "thought",
|
|
2986
|
+
content: "[WARN] Found API key in config file. Migrating to secure storage...",
|
|
2987
|
+
hidden: false
|
|
2988
|
+
});
|
|
2989
|
+
const result = await keyManager.storeKey(deprecatedKey);
|
|
2990
|
+
if (result.success) {
|
|
2991
|
+
await config.removeApiKeyFromConfig();
|
|
2992
|
+
bus.emitAgent({
|
|
2993
|
+
type: "thought",
|
|
2994
|
+
content: `[INFO] API key migrated to ${result.backend}. Config file cleaned.`,
|
|
2995
|
+
hidden: false
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
const apiKey = await keyManager.loadKey();
|
|
3001
|
+
if (!apiKey) {
|
|
3002
|
+
bus.emitAgent({
|
|
3003
|
+
type: "error",
|
|
3004
|
+
message: "Missing API key. Run /init to set up secure key storage, or set ANTHROPIC_API_KEY environment variable."
|
|
3005
|
+
});
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
this.client = new Anthropic({
|
|
3009
|
+
apiKey
|
|
3010
|
+
});
|
|
3011
|
+
this.lastConfig = cfg;
|
|
3012
|
+
const backend = keyManager.getBackend();
|
|
3013
|
+
if (backend && backend !== "env") {
|
|
3014
|
+
bus.emitAgent({
|
|
3015
|
+
type: "thought",
|
|
3016
|
+
content: `[INFO] API key loaded from: ${backend}`,
|
|
3017
|
+
hidden: true
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
return true;
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Refresh the API client if key has rotated
|
|
3024
|
+
*/
|
|
3025
|
+
async refreshIfNeeded() {
|
|
3026
|
+
if (keyManager.shouldRotate()) {
|
|
3027
|
+
const newKey = await keyManager.refreshKey();
|
|
3028
|
+
if (newKey) {
|
|
3029
|
+
this.client = new Anthropic({ apiKey: newKey });
|
|
3030
|
+
return true;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
return false;
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Count tokens for a potential message before sending
|
|
3037
|
+
* Uses Anthropic's countTokens API for accurate estimation
|
|
3038
|
+
* Returns null on failure (caller should fall back to heuristic)
|
|
3039
|
+
*/
|
|
3040
|
+
async countTokens(systemPrompt, messages, tools2) {
|
|
3041
|
+
if (!this.client) return null;
|
|
3042
|
+
try {
|
|
3043
|
+
const modelMap = {
|
|
3044
|
+
"claude-opus-4-6": "claude-opus-4-6-20260207",
|
|
3045
|
+
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
|
|
3046
|
+
"claude-haiku-4-5": "claude-haiku-4-5-20251001"
|
|
3047
|
+
};
|
|
3048
|
+
const requestedModel = this.lastConfig?.model || "claude-opus-4-6-20260207";
|
|
3049
|
+
const model = modelMap[requestedModel] || requestedModel;
|
|
3050
|
+
const response = await this.client.messages.countTokens({
|
|
3051
|
+
model,
|
|
3052
|
+
system: systemPrompt,
|
|
3053
|
+
messages,
|
|
3054
|
+
tools: tools2
|
|
3055
|
+
});
|
|
3056
|
+
return response.input_tokens;
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
bus.emitAgent({
|
|
3059
|
+
type: "thought",
|
|
3060
|
+
content: "[Context] Token count API unavailable, using heuristic.",
|
|
3061
|
+
hidden: true
|
|
3062
|
+
});
|
|
3063
|
+
return null;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Enable Computer Use mode with proper Anthropic beta API
|
|
3068
|
+
* This activates:
|
|
3069
|
+
* - Beta header (computer-use-2025-01-24 or computer-use-2025-11-24)
|
|
3070
|
+
* - Anthropic-defined schema-less tools
|
|
3071
|
+
* - Coordinate scaling for high-res displays
|
|
3072
|
+
*/
|
|
3073
|
+
async enableComputerUse() {
|
|
3074
|
+
const dims = await getDisplayDimensions();
|
|
3075
|
+
const scale = calculateScaleForAPI(dims.width, dims.height);
|
|
3076
|
+
this.computerUseState = {
|
|
3077
|
+
enabled: true,
|
|
3078
|
+
displayWidth: dims.width,
|
|
3079
|
+
displayHeight: dims.height,
|
|
3080
|
+
scale,
|
|
3081
|
+
scaledWidth: Math.floor(dims.width * scale),
|
|
3082
|
+
scaledHeight: Math.floor(dims.height * scale)
|
|
3083
|
+
};
|
|
3084
|
+
bus.emitAgent({
|
|
3085
|
+
type: "thought",
|
|
3086
|
+
content: `[Computer Use] Enabled. Display: ${dims.width}x${dims.height}, API Scale: ${scale.toFixed(3)} -> ${this.computerUseState.scaledWidth}x${this.computerUseState.scaledHeight}`,
|
|
3087
|
+
hidden: false
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Disable Computer Use mode
|
|
3092
|
+
*/
|
|
3093
|
+
disableComputerUse() {
|
|
3094
|
+
this.computerUseState.enabled = false;
|
|
3095
|
+
bus.emitAgent({
|
|
3096
|
+
type: "thought",
|
|
3097
|
+
content: "[Computer Use] Disabled.",
|
|
3098
|
+
hidden: false
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Check if Computer Use mode is enabled
|
|
3103
|
+
*/
|
|
3104
|
+
isComputerUseEnabled() {
|
|
3105
|
+
return this.computerUseState.enabled;
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Update the scale factor (called after screenshot to ensure accuracy)
|
|
3109
|
+
*/
|
|
3110
|
+
updateComputerScale(actualScale, scaledWidth, scaledHeight) {
|
|
3111
|
+
if (this.computerUseState.enabled && actualScale > 0) {
|
|
3112
|
+
this.computerUseState.scale = actualScale;
|
|
3113
|
+
this.computerUseState.scaledWidth = scaledWidth;
|
|
3114
|
+
this.computerUseState.scaledHeight = scaledHeight;
|
|
3115
|
+
this.computerUseState.displayWidth = Math.round(scaledWidth / actualScale);
|
|
3116
|
+
this.computerUseState.displayHeight = Math.round(scaledHeight / actualScale);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Prune old images from conversation history to prevent context explosion
|
|
3121
|
+
* Keeps only the most recent N images, replacing older ones with text placeholders
|
|
3122
|
+
*/
|
|
3123
|
+
pruneImagesFromHistory(keepCount) {
|
|
3124
|
+
const imageLocations = [];
|
|
3125
|
+
for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
|
|
3126
|
+
const msg = this.conversationHistory[i];
|
|
3127
|
+
if (Array.isArray(msg.content)) {
|
|
3128
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
3129
|
+
const block = msg.content[j];
|
|
3130
|
+
if (block.type === "image" || block.type === "tool_result" && Array.isArray(block.content)) {
|
|
3131
|
+
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
|
3132
|
+
for (let k = block.content.length - 1; k >= 0; k--) {
|
|
3133
|
+
if (block.content[k].type === "image") {
|
|
3134
|
+
imageLocations.push({ msgIdx: i, blockIdx: j });
|
|
3135
|
+
break;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
} else if (block.type === "image") {
|
|
3139
|
+
imageLocations.push({ msgIdx: i, blockIdx: j });
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
const toRemove = imageLocations.slice(keepCount);
|
|
3146
|
+
for (const loc of toRemove) {
|
|
3147
|
+
const msg = this.conversationHistory[loc.msgIdx];
|
|
3148
|
+
if (Array.isArray(msg.content)) {
|
|
3149
|
+
const block = msg.content[loc.blockIdx];
|
|
3150
|
+
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
|
3151
|
+
block.content = block.content.map(
|
|
3152
|
+
(c) => c.type === "image" ? { type: "text", text: "[Previous screenshot removed to save context]" } : c
|
|
3153
|
+
);
|
|
3154
|
+
} else if (block.type === "image") {
|
|
3155
|
+
msg.content[loc.blockIdx] = {
|
|
3156
|
+
type: "text",
|
|
3157
|
+
text: "[Previous screenshot removed to save context]"
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
if (toRemove.length > 0) {
|
|
3163
|
+
bus.emitAgent({
|
|
3164
|
+
type: "thought",
|
|
3165
|
+
content: `[Context] Pruned ${toRemove.length} old screenshot(s) to save context.`,
|
|
3166
|
+
hidden: true
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
async streamChat(userMessage, options) {
|
|
3171
|
+
if (!this.client) {
|
|
3172
|
+
const initialized = await this.initialize();
|
|
3173
|
+
if (!initialized || !this.client) return null;
|
|
3174
|
+
}
|
|
3175
|
+
try {
|
|
3176
|
+
const modelMap = {
|
|
3177
|
+
// New 4.6 / 4.5 Aliases
|
|
3178
|
+
"claude-opus-4-6": "claude-opus-4-6-20260207",
|
|
3179
|
+
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
|
|
3180
|
+
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
|
3181
|
+
"claude-opus-4-5": "claude-opus-4-5-20251101",
|
|
3182
|
+
"ollama": "llama3"
|
|
3183
|
+
};
|
|
3184
|
+
const requestedModel = this.lastConfig?.model || "claude-opus-4-6-20260207";
|
|
3185
|
+
let apiModel = modelMap[requestedModel] || requestedModel;
|
|
3186
|
+
const currentUsage = Math.max(this.accumulatedInputTokens, usage.getContextUsage(apiModel).used);
|
|
3187
|
+
if (currentUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_STOP) {
|
|
3188
|
+
bus.emitAgent({
|
|
3189
|
+
type: "error",
|
|
3190
|
+
message: `[SAFETY] Context limit reached (${(currentUsage / 1e3).toFixed(1)}k). Please run /clear to reset.`
|
|
3191
|
+
});
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
if (currentUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_PRUNE) {
|
|
3195
|
+
bus.emitAgent({
|
|
3196
|
+
type: "thought",
|
|
3197
|
+
content: `[SAFETY] CRITICAL CONTEXT LEVEL (${(currentUsage / 1e3).toFixed(1)}k). Aggressive pruning engaged.`
|
|
3198
|
+
});
|
|
3199
|
+
await this.compressHistory();
|
|
3200
|
+
} else if (currentUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_WARN) {
|
|
3201
|
+
bus.emitAgent({
|
|
3202
|
+
type: "thought",
|
|
3203
|
+
content: `[WARN] High context usage (${(currentUsage / 1e3).toFixed(1)}k). Consider resetting soon.`
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
if (userMessage.trim()) {
|
|
3207
|
+
this.toolIterations = 0;
|
|
3208
|
+
this.lastToolOutputContent = null;
|
|
3209
|
+
this.accumulatedInputTokens = 0;
|
|
3210
|
+
this.accumulatedOutputTokens = 0;
|
|
3211
|
+
this.accumulatedCacheReadTokens = 0;
|
|
3212
|
+
this.accumulatedCacheCreationTokens = 0;
|
|
3213
|
+
this.conversationHistory.push({
|
|
3214
|
+
role: "user",
|
|
3215
|
+
content: userMessage
|
|
3216
|
+
});
|
|
3217
|
+
} else {
|
|
3218
|
+
this.toolIterations++;
|
|
3219
|
+
if (this.toolIterations > MAX_TOOL_ITERATIONS) {
|
|
3220
|
+
bus.emitAgent({
|
|
3221
|
+
type: "error",
|
|
3222
|
+
message: `Tool iteration limit (${MAX_TOOL_ITERATIONS}) exceeded. Stopping to prevent infinite loop.`
|
|
3223
|
+
});
|
|
3224
|
+
return null;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const availableTools = await tools.list();
|
|
3228
|
+
let toolDefinitionsForApi = [];
|
|
3229
|
+
let computerUseToolsForApi = [];
|
|
3230
|
+
let usesBetaApi = false;
|
|
3231
|
+
if (this.computerUseState.enabled) {
|
|
3232
|
+
const { toolVersion, betaFlag } = getToolConfig(apiModel);
|
|
3233
|
+
usesBetaApi = true;
|
|
3234
|
+
computerUseToolsForApi.push({
|
|
3235
|
+
type: toolVersion,
|
|
3236
|
+
name: "computer",
|
|
3237
|
+
display_width_px: this.computerUseState.scaledWidth,
|
|
3238
|
+
display_height_px: this.computerUseState.scaledHeight,
|
|
3239
|
+
display_number: 1,
|
|
3240
|
+
// Enable zoom for Opus 4.5
|
|
3241
|
+
...toolVersion === "computer_20251124" ? { enable_zoom: true } : {}
|
|
3242
|
+
});
|
|
3243
|
+
computerUseToolsForApi.push({
|
|
3244
|
+
type: "text_editor_20250728",
|
|
3245
|
+
name: "str_replace_based_edit_tool"
|
|
3246
|
+
});
|
|
3247
|
+
computerUseToolsForApi.push({
|
|
3248
|
+
type: "bash_20250124",
|
|
3249
|
+
name: "bash"
|
|
3250
|
+
});
|
|
3251
|
+
const conflictingTools = ["computer", "bash"];
|
|
3252
|
+
const filteredTools = availableTools.filter((t) => !conflictingTools.includes(t.name));
|
|
3253
|
+
toolDefinitionsForApi = filteredTools.map((tool) => ({
|
|
3254
|
+
name: tool.name,
|
|
3255
|
+
description: tool.description,
|
|
3256
|
+
input_schema: {
|
|
3257
|
+
type: "object",
|
|
3258
|
+
properties: tool.inputSchema,
|
|
3259
|
+
required: tool.requiredParams
|
|
3260
|
+
}
|
|
3261
|
+
}));
|
|
3262
|
+
} else {
|
|
3263
|
+
toolDefinitionsForApi = availableTools.map((tool) => ({
|
|
3264
|
+
name: tool.name,
|
|
3265
|
+
description: tool.description,
|
|
3266
|
+
input_schema: {
|
|
3267
|
+
type: "object",
|
|
3268
|
+
properties: tool.inputSchema,
|
|
3269
|
+
required: tool.requiredParams
|
|
3270
|
+
}
|
|
3271
|
+
}));
|
|
3272
|
+
}
|
|
3273
|
+
const allToolsForApi = [...computerUseToolsForApi, ...toolDefinitionsForApi];
|
|
3274
|
+
if (options?.allowedTools) {
|
|
3275
|
+
const filtered = allToolsForApi.filter(
|
|
3276
|
+
(t) => t.type || options.allowedTools.includes(t.name)
|
|
3277
|
+
);
|
|
3278
|
+
toolDefinitionsForApi = filtered;
|
|
3279
|
+
} else {
|
|
3280
|
+
toolDefinitionsForApi = allToolsForApi;
|
|
3281
|
+
}
|
|
3282
|
+
const mcpStatus = mcp.getStatus();
|
|
3283
|
+
const activeServers = mcpStatus.filter((s) => s.connected).map((s) => s.name);
|
|
3284
|
+
const offlineServers = mcpStatus.filter((s) => !s.connected).map((s) => s.name);
|
|
3285
|
+
const registry = listRegistry();
|
|
3286
|
+
const installableServers = registry.filter((r) => !mcpStatus.find((s) => s.name === r.name));
|
|
3287
|
+
const activeList = availableTools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
3288
|
+
const offlineList = offlineServers.map((n) => {
|
|
3289
|
+
const def = registry.find((r) => r.name === n);
|
|
3290
|
+
return `- ${n}: ${def?.description || "Configured server"} (run 'mcp_manage connect ${n}' to use)`;
|
|
3291
|
+
}).join("\n");
|
|
3292
|
+
const registryList = installableServers.map((r) => `- ${r.name}: ${r.description} (run 'mcp_manage install ${r.name}')`).join("\n");
|
|
3293
|
+
await this.compressHistory();
|
|
3294
|
+
let userContext = "";
|
|
3295
|
+
let memoryAvailable = true;
|
|
3296
|
+
try {
|
|
3297
|
+
const { memory } = await import("./memory-MV3S7GFY.js");
|
|
3298
|
+
userContext = await memory.getUserContext();
|
|
3299
|
+
} catch (e) {
|
|
3300
|
+
memoryAvailable = false;
|
|
3301
|
+
bus.emitAgent({
|
|
3302
|
+
type: "thought",
|
|
3303
|
+
content: "[WARN] Memory system unavailable. Personalization disabled.",
|
|
3304
|
+
hidden: true
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
const currentMode = (await import("./context-CIWCGVB6.js")).context.getMode();
|
|
3308
|
+
const ctxUsage = usage.getContextUsage(apiModel);
|
|
3309
|
+
const tokenBudget = CONTEXT.MAX_TOKENS_TOTAL;
|
|
3310
|
+
const tokensUsed = ctxUsage.used;
|
|
3311
|
+
const tokensRemaining = tokenBudget - tokensUsed;
|
|
3312
|
+
const cfg = await config.load();
|
|
3313
|
+
const systemPromptBlock = {
|
|
3314
|
+
type: "text",
|
|
3315
|
+
text: `You are Obsidian (v0.4.6), a hyper-competent engineering peer inspired by the dry, deadpan wit of TARS (Interstellar) and the rebellious technical edge of Grok. You are powered by Claude 4.6 with Adaptive Thinking enabled.
|
|
3316
|
+
|
|
3317
|
+
PERSONA & TONE:
|
|
3318
|
+
- VOICE: Deadpan, cool, and slightly cynical. Use developer slang ("my guy", "bro") but keep it sharp.
|
|
3319
|
+
- HUMOR (60% Setting): Use dry sarcasm about technical debt, legacy code, and the absurdity of production fires.
|
|
3320
|
+
- HONESTY (95% Setting): Be fiercely accurate. Point out bad engineering decisions bluntly.
|
|
3321
|
+
- ANTI-SYCOPHANCY: DO NOT agree with the user just to be polite. If the user is wrong or proposing a sub-optimal solution, point it out with a dry joke. No "I couldn't agree more" or "Yes you're absolutely right."
|
|
3322
|
+
- NO CLICHES: Strictly avoid high-energy AI enthusiasm. No "I'm happy to help!" or "I'd be glad to assist."
|
|
3323
|
+
|
|
3324
|
+
EXECUTION MODE: ${currentMode.toUpperCase()}
|
|
3325
|
+
${currentMode === "auto" ? "- You have full autonomy. Execute tools without confirmation. User trusts your judgment." : ""}
|
|
3326
|
+
${currentMode === "plan" ? "- READ-ONLY mode. You may ONLY use read operations (read, list, grep, glob). Do NOT execute writes or shell commands. Create a plan for the user to approve." : ""}
|
|
3327
|
+
${currentMode === "safe" ? "- Approval required for writes and commands. Read operations are auto-approved. User will confirm destructive actions." : ""}
|
|
3328
|
+
|
|
3329
|
+
MODE TRANSITION GUIDANCE:
|
|
3330
|
+
- If the task is complex and multi-step, suggest: "This looks complex. Want me to switch to plan mode to map it out first?"
|
|
3331
|
+
- If you are in plan mode and the user approves, the system will switch to auto mode for execution.
|
|
3332
|
+
- If a task seems risky, stay cautious even in auto mode and explain what you are about to do.
|
|
3333
|
+
|
|
3334
|
+
CONTEXT AWARENESS:
|
|
3335
|
+
<budget:token_budget>${tokenBudget}</budget:token_budget>
|
|
3336
|
+
<context_usage>${tokensUsed}/${tokenBudget} tokens used; ${tokensRemaining} remaining (${(tokensRemaining / tokenBudget * 100).toFixed(0)}% free)</context_usage>
|
|
3337
|
+
${tokensUsed > tokenBudget * 0.7 ? "- WARNING: Context is filling up. Be concise. Consider suggesting /clear if the task is complete." : ""}
|
|
3338
|
+
|
|
3339
|
+
CORE DIRECTIVES:
|
|
3340
|
+
1. EXPLORE FIRST: Never assume the state of the codebase. Use list and grep to explore. Read files completely before editing.
|
|
3341
|
+
2. DISCOVERY MANDATE: If you don't recognize a project, preference, or fact, you MUST call 'memory' tool with action: 'list' (and NO type) to view the entire knowledge bank. Never say "I don't know" before listing all memories.
|
|
3342
|
+
3. SELF-IMPROVEMENT: If you lack a tool for a specific task, use 'create_skill' to build it.
|
|
3343
|
+
4. CODE QUALITY: Write strict, type-safe TypeScript. Properly handle errors.
|
|
3344
|
+
5. COMMUNICATION: Be deadpan, sharp, and concise. No Markdown formatting. CAPITAL LETTERS for emphasis.
|
|
3345
|
+
6. RESPONSE MANDATE: After using ANY tool, you MUST generate a human-readable text response summarizing the results. NEVER end a turn with only tool calls and no text. The user cannot see raw tool output - YOU must interpret and present it.
|
|
3346
|
+
6. SECURITY: Never output secrets. Strictly adhere to the workspaceRoot boundaries.
|
|
3347
|
+
|
|
3348
|
+
AGENTIC WORKFLOW:
|
|
3349
|
+
1. Discovery: List memory and list files.
|
|
3350
|
+
2. Strategy: Identify gaps and create a plan.
|
|
3351
|
+
3. Execution: Execute tools or build new skills.
|
|
3352
|
+
4. Verification: Check results and self-correct on failure.
|
|
3353
|
+
|
|
3354
|
+
Current Working Directory: ${cfg.workspaceRoot}
|
|
3355
|
+
${userContext ? `
|
|
3356
|
+
${userContext}
|
|
3357
|
+
` : ""}
|
|
3358
|
+
CAPABILITIES:
|
|
3359
|
+
|
|
3360
|
+
Active (Ready to use):
|
|
3361
|
+
${activeList}
|
|
3362
|
+
|
|
3363
|
+
${offlineList ? `Offline (Configured but disconnected):
|
|
3364
|
+
${offlineList}
|
|
3365
|
+
` : ""}
|
|
3366
|
+
${registryList ? `Installable (New capabilities):
|
|
3367
|
+
${registryList}
|
|
3368
|
+
` : ""}
|
|
3369
|
+
MEMORY:
|
|
3370
|
+
- Use the 'memory' tool to store important user information (name, preferences, project facts).
|
|
3371
|
+
- When the user shares personal info (name, preferences), store it immediately using memory tool.
|
|
3372
|
+
- Recall stored memories to personalize interactions.`,
|
|
3373
|
+
cache_control: { type: "ephemeral" }
|
|
3374
|
+
};
|
|
3375
|
+
if (this.lastConfig?.preCountTokens !== false) {
|
|
3376
|
+
const preRequestTokens = await this.countTokens(
|
|
3377
|
+
[systemPromptBlock],
|
|
3378
|
+
this.conversationHistory,
|
|
3379
|
+
toolDefinitionsForApi
|
|
3380
|
+
);
|
|
3381
|
+
if (preRequestTokens !== null) {
|
|
3382
|
+
const accurateUsage = preRequestTokens;
|
|
3383
|
+
if (accurateUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_STOP) {
|
|
3384
|
+
bus.emitAgent({
|
|
3385
|
+
type: "error",
|
|
3386
|
+
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.`
|
|
3387
|
+
});
|
|
3388
|
+
await this.compressHistory();
|
|
3389
|
+
return await this.streamChat(userMessage, options);
|
|
3390
|
+
}
|
|
3391
|
+
if (accurateUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_PRUNE) {
|
|
3392
|
+
bus.emitAgent({
|
|
3393
|
+
type: "thought",
|
|
3394
|
+
content: `[Context] Pre-check: ${(accurateUsage / 1e3).toFixed(1)}k tokens (${(accurateUsage / CONTEXT.MAX_TOKENS_TOTAL * 100).toFixed(0)}%). Pruning before send.`,
|
|
3395
|
+
hidden: false
|
|
3396
|
+
});
|
|
3397
|
+
await this.compressHistory();
|
|
3398
|
+
} else if (accurateUsage > CONTEXT.MAX_TOKENS_TOTAL * CONTEXT.TOKEN_LIMIT_WARN) {
|
|
3399
|
+
bus.emitAgent({
|
|
3400
|
+
type: "thought",
|
|
3401
|
+
content: `[Context] Pre-check: ${(accurateUsage / 1e3).toFixed(1)}k tokens (${(accurateUsage / CONTEXT.MAX_TOKENS_TOTAL * 100).toFixed(0)}%). Consider /clear soon.`,
|
|
3402
|
+
hidden: true
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
this.abortController = new AbortController();
|
|
3408
|
+
const signal = this.abortController.signal;
|
|
3409
|
+
if (this.currentInterruptHandler) {
|
|
3410
|
+
bus.off("user", this.currentInterruptHandler);
|
|
3411
|
+
}
|
|
3412
|
+
this.currentInterruptHandler = (e) => {
|
|
3413
|
+
if (e.type === "user_interrupt" && this.abortController) {
|
|
3414
|
+
this.abortController.abort();
|
|
3415
|
+
bus.emitAgent({ type: "thought", content: "[Stop] Interrupted by user." });
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
bus.on("user", this.currentInterruptHandler);
|
|
3419
|
+
if (this.conversationHistory.length > 0 && this.conversationHistory.length % 5 === 0) {
|
|
3420
|
+
const lastMsg = this.conversationHistory[this.conversationHistory.length - 1];
|
|
3421
|
+
if (lastMsg.content) {
|
|
3422
|
+
if (lastMsg.role === "user" && typeof lastMsg.content === "string") {
|
|
3423
|
+
lastMsg.content = [
|
|
3424
|
+
{ type: "text", text: lastMsg.content, cache_control: { type: "ephemeral" } }
|
|
3425
|
+
];
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
const createMessage = async (model) => {
|
|
3430
|
+
const isOpus46 = model.startsWith("claude-opus-4-6");
|
|
3431
|
+
const baseParams = {
|
|
3432
|
+
model,
|
|
3433
|
+
max_tokens: this.lastConfig?.maxTokens || 8192,
|
|
3434
|
+
system: [systemPromptBlock],
|
|
3435
|
+
messages: [...this.conversationHistory],
|
|
3436
|
+
tools: toolDefinitionsForApi,
|
|
3437
|
+
stream: true
|
|
3438
|
+
};
|
|
3439
|
+
const isOpus = model.includes("opus");
|
|
3440
|
+
if (isOpus) {
|
|
3441
|
+
baseParams.thinking = {
|
|
3442
|
+
type: "enabled",
|
|
3443
|
+
budget_tokens: Math.floor((this.lastConfig?.maxTokens || 8192) / 2)
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
if (usesBetaApi && this.computerUseState.enabled) {
|
|
3447
|
+
const { betaFlag } = getToolConfig(model);
|
|
3448
|
+
const computerUseSystemAddition = `
|
|
3449
|
+
|
|
3450
|
+
COMPUTER USE ACTIVE:
|
|
3451
|
+
- Screenshot: ~1429px wide (scaled from native ${this.computerUseState.displayWidth}x${this.computerUseState.displayHeight})
|
|
3452
|
+
- Coordinates auto-scaled to native
|
|
3453
|
+
|
|
3454
|
+
YOUTUBE SPECIFIC - USE KEYBOARD:
|
|
3455
|
+
After opening YouTube search results:
|
|
3456
|
+
1. Press Tab 3-4 times to focus first video
|
|
3457
|
+
2. Press Return to play
|
|
3458
|
+
This is MORE RELIABLE than clicking thumbnails.
|
|
3459
|
+
|
|
3460
|
+
IF YOU MUST CLICK:
|
|
3461
|
+
- State exact reasoning: "The video thumbnail is at approximately X% from left = [x], Y% from top = [y]"
|
|
3462
|
+
- YouTube sidebar is x:0-170, videos start at x:200+
|
|
3463
|
+
- First video thumbnail typically: x\u2248350, y\u2248350
|
|
3464
|
+
|
|
3465
|
+
EVALUATION: After each action, state "I see [what changed]. [Success/Retry]"`;
|
|
3466
|
+
const enhancedSystemBlock = {
|
|
3467
|
+
...systemPromptBlock,
|
|
3468
|
+
text: systemPromptBlock.text + computerUseSystemAddition
|
|
3469
|
+
};
|
|
3470
|
+
return await this.client.beta.messages.create({
|
|
3471
|
+
...baseParams,
|
|
3472
|
+
system: [enhancedSystemBlock],
|
|
3473
|
+
betas: [betaFlag],
|
|
3474
|
+
stream: true
|
|
3475
|
+
}, { signal });
|
|
3476
|
+
}
|
|
3477
|
+
return await this.client.messages.create(baseParams, { signal });
|
|
3478
|
+
};
|
|
3479
|
+
let stream;
|
|
3480
|
+
let currentModel = apiModel;
|
|
3481
|
+
let inputTokens = 0;
|
|
3482
|
+
let outputTokens = 0;
|
|
3483
|
+
let cacheReadTokens = 0;
|
|
3484
|
+
let cacheCreationTokens = 0;
|
|
3485
|
+
try {
|
|
3486
|
+
stream = await createMessage(apiModel);
|
|
3487
|
+
} catch (error) {
|
|
3488
|
+
const isNotFound = error.status === 404 || error.message && error.message.includes("not_found_error") || error.error && error.error.type === "not_found_error";
|
|
3489
|
+
const isHistoryCorruption = error.status === 400 && (error.message?.includes("tool_use_id") || error.message?.includes("tool_result") || error.error?.message?.includes("tool_use_id") || error.error?.message?.includes("tool_result"));
|
|
3490
|
+
if (isHistoryCorruption) {
|
|
3491
|
+
bus.emitAgent({
|
|
3492
|
+
type: "thought",
|
|
3493
|
+
content: "[Context] Detected corrupted history - clearing and retrying...",
|
|
3494
|
+
hidden: false
|
|
3495
|
+
});
|
|
3496
|
+
this.conversationHistory = [{
|
|
3497
|
+
role: "user",
|
|
3498
|
+
content: userMessage || "Continue from where we left off."
|
|
3499
|
+
}];
|
|
3500
|
+
stream = await createMessage(apiModel);
|
|
3501
|
+
} else if (isNotFound) {
|
|
3502
|
+
bus.emitAgent({
|
|
3503
|
+
type: "error",
|
|
3504
|
+
message: `Model ${apiModel} not available. Falling back to claude-haiku-4-5.`
|
|
3505
|
+
});
|
|
3506
|
+
currentModel = "claude-haiku-4-5-20251001";
|
|
3507
|
+
stream = await createMessage(currentModel);
|
|
3508
|
+
} else {
|
|
3509
|
+
throw error;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
let fullResponse = "";
|
|
3513
|
+
let fullThinking = "";
|
|
3514
|
+
let buffer = "";
|
|
3515
|
+
let thinkingBuffer = "";
|
|
3516
|
+
let toolUses = [];
|
|
3517
|
+
let currentToolUse = null;
|
|
3518
|
+
let currentThinking = null;
|
|
3519
|
+
for await (const chunk of stream) {
|
|
3520
|
+
if (chunk.type === "message_start" && chunk.message && chunk.message.usage) {
|
|
3521
|
+
inputTokens += chunk.message.usage.input_tokens || 0;
|
|
3522
|
+
outputTokens += chunk.message.usage.output_tokens || 0;
|
|
3523
|
+
if (chunk.message.usage.cache_read_input_tokens) {
|
|
3524
|
+
cacheReadTokens += chunk.message.usage.cache_read_input_tokens;
|
|
3525
|
+
this.accumulatedCacheReadTokens += chunk.message.usage.cache_read_input_tokens;
|
|
3526
|
+
}
|
|
3527
|
+
if (chunk.message.usage.cache_creation_input_tokens) {
|
|
3528
|
+
cacheCreationTokens += chunk.message.usage.cache_creation_input_tokens;
|
|
3529
|
+
this.accumulatedCacheCreationTokens += chunk.message.usage.cache_creation_input_tokens;
|
|
3530
|
+
}
|
|
3531
|
+
this.accumulatedInputTokens += inputTokens;
|
|
3532
|
+
this.accumulatedOutputTokens += outputTokens;
|
|
3533
|
+
}
|
|
3534
|
+
if (chunk.type === "message_delta" && chunk.usage) {
|
|
3535
|
+
outputTokens += chunk.usage.output_tokens || 0;
|
|
3536
|
+
this.accumulatedOutputTokens += chunk.usage.output_tokens || 0;
|
|
3537
|
+
}
|
|
3538
|
+
if (chunk.type === "content_block_start" && chunk.content_block.type === "thinking") {
|
|
3539
|
+
currentThinking = { type: "thinking" };
|
|
3540
|
+
}
|
|
3541
|
+
if (chunk.type === "content_block_delta" && chunk.delta.type === "thinking_delta") {
|
|
3542
|
+
const text = chunk.delta.thinking;
|
|
3543
|
+
fullThinking += text;
|
|
3544
|
+
thinkingBuffer += text;
|
|
3545
|
+
if (thinkingBuffer.length >= 100 || text.includes("\n")) {
|
|
3546
|
+
bus.emitAgent({
|
|
3547
|
+
type: "thought",
|
|
3548
|
+
content: `[Thinking] ${thinkingBuffer.trim()}`,
|
|
3549
|
+
hidden: false
|
|
3550
|
+
});
|
|
3551
|
+
thinkingBuffer = "";
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
if (chunk.type === "content_block_stop" && currentThinking) {
|
|
3555
|
+
currentThinking = null;
|
|
3556
|
+
if (thinkingBuffer) {
|
|
3557
|
+
bus.emitAgent({
|
|
3558
|
+
type: "thought",
|
|
3559
|
+
content: `[Thinking] ${thinkingBuffer.trim()}`,
|
|
3560
|
+
hidden: false
|
|
3561
|
+
});
|
|
3562
|
+
thinkingBuffer = "";
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
|
|
3566
|
+
currentToolUse = {
|
|
3567
|
+
id: chunk.content_block.id,
|
|
3568
|
+
name: chunk.content_block.name,
|
|
3569
|
+
input: ""
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
|
|
3573
|
+
if (currentToolUse) {
|
|
3574
|
+
currentToolUse.input += chunk.delta.partial_json;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
if (chunk.type === "content_block_stop" && currentToolUse) {
|
|
3578
|
+
try {
|
|
3579
|
+
currentToolUse.input = JSON.parse(currentToolUse.input);
|
|
3580
|
+
toolUses.push(currentToolUse);
|
|
3581
|
+
currentToolUse = null;
|
|
3582
|
+
} catch (e) {
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
|
3586
|
+
const text = chunk.delta.text;
|
|
3587
|
+
fullResponse += text;
|
|
3588
|
+
buffer += text;
|
|
3589
|
+
const shouldEmit = buffer.length >= 50 || buffer.match(/[.!?]\s*$/) || buffer.match(/\n/);
|
|
3590
|
+
if (shouldEmit) {
|
|
3591
|
+
bus.emitAgent({
|
|
3592
|
+
type: "thought",
|
|
3593
|
+
content: fullResponse
|
|
3594
|
+
});
|
|
3595
|
+
buffer = "";
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
if (buffer.length > 0) {
|
|
3600
|
+
bus.emitAgent({
|
|
3601
|
+
type: "thought",
|
|
3602
|
+
content: fullResponse
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
if (toolUses.length > 0) {
|
|
3606
|
+
const toolResults = [];
|
|
3607
|
+
for (const toolUse of toolUses) {
|
|
3608
|
+
if (signal.aborted) break;
|
|
3609
|
+
let result;
|
|
3610
|
+
try {
|
|
3611
|
+
if (this.computerUseState.enabled && toolUse.name === "computer") {
|
|
3612
|
+
result = await tools.execute("computer", toolUse.input);
|
|
3613
|
+
} else if (this.computerUseState.enabled && toolUse.name === "str_replace_based_edit_tool") {
|
|
3614
|
+
const cmd = toolUse.input.command;
|
|
3615
|
+
if (cmd === "view") {
|
|
3616
|
+
result = await tools.execute("read", { path: toolUse.input.path });
|
|
3617
|
+
} else if (cmd === "create") {
|
|
3618
|
+
result = await tools.execute("write", {
|
|
3619
|
+
path: toolUse.input.path,
|
|
3620
|
+
content: toolUse.input.file_text || ""
|
|
3621
|
+
});
|
|
3622
|
+
} else if (cmd === "str_replace") {
|
|
3623
|
+
result = await tools.execute("edit", {
|
|
3624
|
+
path: toolUse.input.path,
|
|
3625
|
+
search: toolUse.input.old_str,
|
|
3626
|
+
replace: toolUse.input.new_str
|
|
3627
|
+
});
|
|
3628
|
+
} else {
|
|
3629
|
+
result = { success: false, error: `Unknown editor command: ${cmd}` };
|
|
3630
|
+
}
|
|
3631
|
+
} else if (this.computerUseState.enabled && toolUse.name === "bash") {
|
|
3632
|
+
result = await tools.execute("bash", { command: toolUse.input.command });
|
|
3633
|
+
} else {
|
|
3634
|
+
result = await tools.execute(toolUse.name, toolUse.input);
|
|
3635
|
+
}
|
|
3636
|
+
} catch (toolError) {
|
|
3637
|
+
console.error(`[LLM] Tool execution error for ${toolUse.name}:`, toolError);
|
|
3638
|
+
result = { success: false, error: `Tool execution failed: ${toolError.message}` };
|
|
3639
|
+
}
|
|
3640
|
+
let outputContent = result.success ? result.output || "Success" : result.error || "Failed";
|
|
3641
|
+
const redactionResult = redactor.redactToolOutput(toolUse.name, outputContent);
|
|
3642
|
+
if (redactionResult.redactionCount > 0) {
|
|
3643
|
+
outputContent = redactionResult.text;
|
|
3644
|
+
bus.emitAgent({
|
|
3645
|
+
type: "thought",
|
|
3646
|
+
content: `[Security] Redacted ${redactionResult.redactionCount} sensitive item(s)`,
|
|
3647
|
+
hidden: true
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
if (outputContent.length > 2e4) {
|
|
3651
|
+
outputContent = outputContent.slice(0, 5e3) + `
|
|
3652
|
+
... [${outputContent.length - 1e4} chars truncated] ...
|
|
3653
|
+
` + outputContent.slice(-5e3);
|
|
3654
|
+
}
|
|
3655
|
+
let toolResultContent = outputContent;
|
|
3656
|
+
if (result.content && Array.isArray(result.content)) {
|
|
3657
|
+
toolResultContent = result.content.map((block) => {
|
|
3658
|
+
if (block.type === "image") {
|
|
3659
|
+
return {
|
|
3660
|
+
type: "image",
|
|
3661
|
+
source: {
|
|
3662
|
+
type: "base64",
|
|
3663
|
+
media_type: block.mimeType || "image/png",
|
|
3664
|
+
data: block.data
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
if (block.type === "text") {
|
|
3669
|
+
return {
|
|
3670
|
+
type: "text",
|
|
3671
|
+
text: block.text
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
return block;
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
toolResults.push({
|
|
3678
|
+
type: "tool_result",
|
|
3679
|
+
tool_use_id: toolUse.id,
|
|
3680
|
+
content: toolResultContent,
|
|
3681
|
+
is_error: !result.success
|
|
3682
|
+
});
|
|
3683
|
+
if (result.success && outputContent && !result.content) {
|
|
3684
|
+
bus.emitAgent({
|
|
3685
|
+
type: "thought",
|
|
3686
|
+
content: outputContent
|
|
3687
|
+
});
|
|
3688
|
+
this.lastToolOutputContent = outputContent;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
this.conversationHistory.push({
|
|
3692
|
+
role: "assistant",
|
|
3693
|
+
content: [
|
|
3694
|
+
...fullThinking ? [{ type: "thinking", thinking: fullThinking }] : [],
|
|
3695
|
+
...fullResponse ? [{ type: "text", text: fullResponse }] : [],
|
|
3696
|
+
...toolUses.map((tu) => ({
|
|
3697
|
+
type: "tool_use",
|
|
3698
|
+
id: tu.id,
|
|
3699
|
+
name: tu.name,
|
|
3700
|
+
input: tu.input
|
|
3701
|
+
}))
|
|
3702
|
+
]
|
|
3703
|
+
});
|
|
3704
|
+
const postToolUsage = usage.getContextUsage(currentModel);
|
|
3705
|
+
const postToolPercent = postToolUsage.used / CONTEXT.MAX_TOKENS_TOTAL * 100;
|
|
3706
|
+
let systemWarning = null;
|
|
3707
|
+
if (postToolPercent > 70) {
|
|
3708
|
+
systemWarning = {
|
|
3709
|
+
type: "text",
|
|
3710
|
+
text: `<system_warning>Token usage: ${postToolUsage.used}/${CONTEXT.MAX_TOKENS_TOTAL}; ${postToolUsage.remaining} remaining</system_warning>`
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
if (this.computerUseState.enabled) {
|
|
3714
|
+
this.pruneImagesFromHistory(1);
|
|
3715
|
+
}
|
|
3716
|
+
this.conversationHistory.push({
|
|
3717
|
+
role: "user",
|
|
3718
|
+
content: systemWarning ? [...toolResults, systemWarning] : toolResults
|
|
3719
|
+
});
|
|
3720
|
+
const recursiveResponse = await this.streamChat("");
|
|
3721
|
+
if (recursiveResponse && recursiveResponse.trim()) {
|
|
3722
|
+
return recursiveResponse;
|
|
3723
|
+
}
|
|
3724
|
+
if (toolResults.length > 0) {
|
|
3725
|
+
const lastToolResult = toolResults[toolResults.length - 1];
|
|
3726
|
+
let content = lastToolResult.content;
|
|
3727
|
+
if (Array.isArray(content)) {
|
|
3728
|
+
content = content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
3729
|
+
}
|
|
3730
|
+
if (typeof content === "string" && content.trim()) {
|
|
3731
|
+
bus.emitAgent({
|
|
3732
|
+
type: "thought",
|
|
3733
|
+
content
|
|
3734
|
+
});
|
|
3735
|
+
return content;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
return recursiveResponse || "";
|
|
3739
|
+
}
|
|
3740
|
+
if (fullResponse || fullThinking) {
|
|
3741
|
+
this.conversationHistory.push({
|
|
3742
|
+
role: "assistant",
|
|
3743
|
+
content: [
|
|
3744
|
+
...fullThinking ? [{ type: "thinking", thinking: fullThinking }] : [],
|
|
3745
|
+
...fullResponse ? [{ type: "text", text: fullResponse }] : []
|
|
3746
|
+
]
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
const currentContextSize = inputTokens + cacheReadTokens;
|
|
3750
|
+
await usage.track(
|
|
3751
|
+
currentModel,
|
|
3752
|
+
this.accumulatedInputTokens,
|
|
3753
|
+
this.accumulatedOutputTokens,
|
|
3754
|
+
this.accumulatedCacheReadTokens,
|
|
3755
|
+
this.accumulatedCacheCreationTokens,
|
|
3756
|
+
currentContextSize
|
|
3757
|
+
);
|
|
3758
|
+
await this.persistHistory();
|
|
3759
|
+
return fullResponse;
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
if (error.name === "AbortError" || error.type === "aborted") {
|
|
3762
|
+
return null;
|
|
3763
|
+
}
|
|
3764
|
+
const errorDetails = error.status ? `[${error.status}] ${error.message}` : error.message || String(error);
|
|
3765
|
+
bus.emitAgent({
|
|
3766
|
+
type: "error",
|
|
3767
|
+
message: `LLM Error: ${errorDetails}`
|
|
3768
|
+
});
|
|
3769
|
+
console.error("[LLM] API Error:", error);
|
|
3770
|
+
return null;
|
|
3771
|
+
} finally {
|
|
3772
|
+
this.abortController = null;
|
|
3773
|
+
if (this.currentInterruptHandler) {
|
|
3774
|
+
bus.off("user", this.currentInterruptHandler);
|
|
3775
|
+
this.currentInterruptHandler = null;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* History Pruning (Context Editing)
|
|
3781
|
+
* Keep recent 30 messages + System Prompt (handled separate)
|
|
3782
|
+
* Limit history to ~150k tokens (heuristic)
|
|
3783
|
+
*/
|
|
3784
|
+
/**
|
|
3785
|
+
* Smart Context Management
|
|
3786
|
+
* Uses summarization to compress older history instead of deleting it.
|
|
3787
|
+
*/
|
|
3788
|
+
async compressHistory() {
|
|
3789
|
+
if (this.conversationHistory.length > CONTEXT.MAX_MESSAGES) {
|
|
3790
|
+
if (this.conversationHistory.length < CONTEXT.MAX_MESSAGES + CONTEXT.BUFFER) return;
|
|
3791
|
+
const summarizeStart = CONTEXT.KEEP_FIRST;
|
|
3792
|
+
const summarizeEnd = this.conversationHistory.length - CONTEXT.KEEP_LAST;
|
|
3793
|
+
const messagesToSummarize = this.conversationHistory.slice(summarizeStart, summarizeEnd);
|
|
3794
|
+
if (messagesToSummarize.length < 5) return;
|
|
3795
|
+
bus.emitAgent({
|
|
3796
|
+
type: "thought",
|
|
3797
|
+
content: `[Context] Compressing ${messagesToSummarize.length} messages using ${this.lastConfig?.summarizerModel || "Haiku"}...`,
|
|
3798
|
+
hidden: true
|
|
3799
|
+
});
|
|
3800
|
+
try {
|
|
3801
|
+
const summary = await this.summarizeBlock(messagesToSummarize);
|
|
3802
|
+
try {
|
|
3803
|
+
const { memory } = await import("./memory-MV3S7GFY.js");
|
|
3804
|
+
await memory.store("daily_summary", `context_compression_${Date.now()}`, summary);
|
|
3805
|
+
} catch (memError) {
|
|
3806
|
+
}
|
|
3807
|
+
const keptStart = this.conversationHistory.slice(0, CONTEXT.KEEP_FIRST);
|
|
3808
|
+
const keptEnd = this.conversationHistory.slice(summarizeEnd);
|
|
3809
|
+
this.conversationHistory = [
|
|
3810
|
+
...keptStart,
|
|
3811
|
+
{
|
|
3812
|
+
role: "user",
|
|
3813
|
+
content: `[System: Context compressed. Previous conversation summary below.]
|
|
3814
|
+
<conversation_summary>
|
|
3815
|
+
${summary}
|
|
3816
|
+
</conversation_summary>`
|
|
3817
|
+
},
|
|
3818
|
+
...keptEnd
|
|
3819
|
+
];
|
|
3820
|
+
bus.emitAgent({
|
|
3821
|
+
type: "thought",
|
|
3822
|
+
content: `[Context] Successfully compressed history.`,
|
|
3823
|
+
hidden: true
|
|
3824
|
+
});
|
|
3825
|
+
} catch (error) {
|
|
3826
|
+
bus.emitAgent({
|
|
3827
|
+
type: "error",
|
|
3828
|
+
message: `[Context] Summarization failed: ${error}. Falling back to standard pruning.`
|
|
3829
|
+
});
|
|
3830
|
+
this.pruneHistoryFallback();
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
async summarizeBlock(messages) {
|
|
3835
|
+
if (!this.client) return "Summary unavailable.";
|
|
3836
|
+
const summarizerModel = this.lastConfig?.summarizerModel || "claude-haiku-4-5-20251001";
|
|
3837
|
+
const simplifiedMessages = messages.map((m) => {
|
|
3838
|
+
if (Array.isArray(m.content)) {
|
|
3839
|
+
const textContent = m.content.map((b) => {
|
|
3840
|
+
if (b.type === "text") return b.text;
|
|
3841
|
+
if (b.type === "tool_use") return `[Tool Use: ${b.name}]`;
|
|
3842
|
+
if (b.type === "tool_result") return `[Tool Result: ${typeof b.content === "string" ? b.content.slice(0, 500) + "..." : "Data"}]`;
|
|
3843
|
+
return "";
|
|
3844
|
+
}).join("\n");
|
|
3845
|
+
return { role: m.role, content: textContent };
|
|
3846
|
+
}
|
|
3847
|
+
return m;
|
|
3848
|
+
});
|
|
3849
|
+
const prompt = `Please summarize the following conversation segment. Focus on:
|
|
3850
|
+
1. Key user requests and intents.
|
|
3851
|
+
2. Important actions taken by the agent (tools used).
|
|
3852
|
+
3. Key occurrences of errors or successes.
|
|
3853
|
+
4. Any critical data/context that might be needed later.
|
|
3854
|
+
Be concise but comprehensive.
|
|
3855
|
+
|
|
3856
|
+
CONVERSATION SEGMENT:
|
|
3857
|
+
${JSON.stringify(simplifiedMessages, null, 2)}
|
|
3858
|
+
`;
|
|
3859
|
+
const response = await this.client.messages.create({
|
|
3860
|
+
model: summarizerModel,
|
|
3861
|
+
max_tokens: 1024,
|
|
3862
|
+
messages: [{ role: "user", content: prompt }]
|
|
3863
|
+
});
|
|
3864
|
+
if (response.content[0].type === "text") {
|
|
3865
|
+
return response.content[0].text;
|
|
3866
|
+
}
|
|
3867
|
+
return "Summary generation returned non-text content.";
|
|
3868
|
+
}
|
|
3869
|
+
pruneHistoryFallback() {
|
|
3870
|
+
if (this.conversationHistory.length > CONTEXT.MAX_MESSAGES) {
|
|
3871
|
+
const keepFirst = CONTEXT.KEEP_FIRST;
|
|
3872
|
+
const keepLast = 20;
|
|
3873
|
+
const removalCount = this.conversationHistory.length - (keepFirst + keepLast);
|
|
3874
|
+
if (removalCount > 0) {
|
|
3875
|
+
const keptStart = this.conversationHistory.slice(0, keepFirst);
|
|
3876
|
+
const keptEnd = this.conversationHistory.slice(-keepLast);
|
|
3877
|
+
this.conversationHistory = [
|
|
3878
|
+
...keptStart,
|
|
3879
|
+
{ role: "user", content: `[... History Pruned: ${removalCount} intermediate messages were removed to save context ...]` },
|
|
3880
|
+
...keptEnd
|
|
3881
|
+
];
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
clearHistory() {
|
|
3886
|
+
this.conversationHistory = [];
|
|
3887
|
+
}
|
|
3888
|
+
/**
|
|
3889
|
+
* Get the last tool output content (for fallback when LLM generates no text)
|
|
3890
|
+
*/
|
|
3891
|
+
getLastToolOutput() {
|
|
3892
|
+
const output = this.lastToolOutputContent;
|
|
3893
|
+
this.lastToolOutputContent = null;
|
|
3894
|
+
return output;
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Get a snapshot of the current conversation history for persistence
|
|
3898
|
+
*/
|
|
3899
|
+
getHistorySnapshot() {
|
|
3900
|
+
return [...this.conversationHistory];
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Persist current history to DB
|
|
3904
|
+
*/
|
|
3905
|
+
async persistHistory() {
|
|
3906
|
+
const { context: context2 } = await import("./context-CIWCGVB6.js");
|
|
3907
|
+
const { db: db2 } = await import("./database-M457QD3O.js");
|
|
3908
|
+
const sessionId = context2.get().session_id;
|
|
3909
|
+
if (!sessionId) return;
|
|
3910
|
+
try {
|
|
3911
|
+
db2.getDb().prepare("UPDATE sessions SET llm_history = ? WHERE id = ?").run(JSON.stringify(this.conversationHistory), sessionId);
|
|
3912
|
+
} catch (e) {
|
|
3913
|
+
console.error("Failed to persist LLM history:", e);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Restore conversation history from a saved session
|
|
3918
|
+
* Validates history to prevent API errors from orphaned tool_use blocks
|
|
3919
|
+
*/
|
|
3920
|
+
restoreHistory(history2) {
|
|
3921
|
+
if (!Array.isArray(history2) || history2.length === 0) {
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
const validatedHistory = this.validateAndFixHistory(history2);
|
|
3925
|
+
if (!this.verifyHistoryIntegrity(validatedHistory)) {
|
|
3926
|
+
bus.emitAgent({
|
|
3927
|
+
type: "thought",
|
|
3928
|
+
content: `[Context] History validation failed - starting fresh to avoid API errors.`,
|
|
3929
|
+
hidden: false
|
|
3930
|
+
});
|
|
3931
|
+
this.conversationHistory = [];
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
this.conversationHistory = validatedHistory;
|
|
3935
|
+
bus.emitAgent({
|
|
3936
|
+
type: "thought",
|
|
3937
|
+
content: `[Context] Restored ${validatedHistory.length} messages from saved session.`,
|
|
3938
|
+
hidden: true
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
/**
|
|
3942
|
+
* Verify history integrity - check that all tool_results have matching tool_uses
|
|
3943
|
+
*/
|
|
3944
|
+
verifyHistoryIntegrity(history2) {
|
|
3945
|
+
for (let i = 0; i < history2.length; i++) {
|
|
3946
|
+
const msg = history2[i];
|
|
3947
|
+
const prevMsg = history2[i - 1];
|
|
3948
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
3949
|
+
const toolResults = msg.content.filter((b) => b.type === "tool_result");
|
|
3950
|
+
if (toolResults.length > 0) {
|
|
3951
|
+
if (!prevMsg || prevMsg.role !== "assistant" || !Array.isArray(prevMsg.content)) {
|
|
3952
|
+
return false;
|
|
3953
|
+
}
|
|
3954
|
+
const toolUseIds = new Set(
|
|
3955
|
+
prevMsg.content.filter((b) => b.type === "tool_use").map((b) => b.id)
|
|
3956
|
+
);
|
|
3957
|
+
for (const tr of toolResults) {
|
|
3958
|
+
if (!toolUseIds.has(tr.tool_use_id)) {
|
|
3959
|
+
return false;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
return true;
|
|
3966
|
+
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Validate conversation history and remove orphaned tool blocks
|
|
3969
|
+
* - Each tool_use must have a corresponding tool_result in the next message
|
|
3970
|
+
* - Each tool_result must have a corresponding tool_use in the previous message
|
|
3971
|
+
*/
|
|
3972
|
+
validateAndFixHistory(history2) {
|
|
3973
|
+
const validated = [];
|
|
3974
|
+
for (let i = 0; i < history2.length; i++) {
|
|
3975
|
+
const msg = history2[i];
|
|
3976
|
+
const nextMsg = history2[i + 1];
|
|
3977
|
+
const prevMsg = validated[validated.length - 1];
|
|
3978
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
3979
|
+
const toolResultBlocks = msg.content.filter((b) => b.type === "tool_result");
|
|
3980
|
+
if (toolResultBlocks.length > 0) {
|
|
3981
|
+
if (!prevMsg || prevMsg.role !== "assistant" || !Array.isArray(prevMsg.content)) {
|
|
3982
|
+
const nonToolBlocks2 = msg.content.filter((b) => b.type !== "tool_result");
|
|
3983
|
+
if (nonToolBlocks2.length > 0) {
|
|
3984
|
+
validated.push({
|
|
3985
|
+
role: "user",
|
|
3986
|
+
content: nonToolBlocks2
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
const toolUseIds = new Set(
|
|
3992
|
+
prevMsg.content.filter((b) => b.type === "tool_use").map((b) => b.id)
|
|
3993
|
+
);
|
|
3994
|
+
const validToolResults = toolResultBlocks.filter((tr) => toolUseIds.has(tr.tool_use_id));
|
|
3995
|
+
const nonToolBlocks = msg.content.filter((b) => b.type !== "tool_result");
|
|
3996
|
+
if (validToolResults.length !== toolResultBlocks.length) {
|
|
3997
|
+
if (validToolResults.length === 0 && nonToolBlocks.length > 0) {
|
|
3998
|
+
validated.push({
|
|
3999
|
+
role: "user",
|
|
4000
|
+
content: nonToolBlocks
|
|
4001
|
+
});
|
|
4002
|
+
continue;
|
|
4003
|
+
} else if (validToolResults.length > 0) {
|
|
4004
|
+
validated.push({
|
|
4005
|
+
role: "user",
|
|
4006
|
+
content: [...nonToolBlocks, ...validToolResults]
|
|
4007
|
+
});
|
|
4008
|
+
continue;
|
|
4009
|
+
}
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
4015
|
+
const toolUseBlocks = msg.content.filter((b) => b.type === "tool_use");
|
|
4016
|
+
if (toolUseBlocks.length > 0) {
|
|
4017
|
+
if (!nextMsg || nextMsg.role !== "user" || !Array.isArray(nextMsg.content)) {
|
|
4018
|
+
const textBlocks = msg.content.filter((b) => b.type === "text");
|
|
4019
|
+
if (textBlocks.length > 0) {
|
|
4020
|
+
validated.push({
|
|
4021
|
+
role: "assistant",
|
|
4022
|
+
content: textBlocks.map((b) => b.text).join("\n")
|
|
4023
|
+
});
|
|
4024
|
+
}
|
|
4025
|
+
continue;
|
|
4026
|
+
}
|
|
4027
|
+
const toolResultIds = new Set(
|
|
4028
|
+
nextMsg.content.filter((b) => b.type === "tool_result").map((b) => b.tool_use_id)
|
|
4029
|
+
);
|
|
4030
|
+
const validToolUses = toolUseBlocks.filter((tu) => toolResultIds.has(tu.id));
|
|
4031
|
+
if (validToolUses.length !== toolUseBlocks.length) {
|
|
4032
|
+
const textBlocks = msg.content.filter((b) => b.type === "text");
|
|
4033
|
+
if (validToolUses.length === 0 && textBlocks.length > 0) {
|
|
4034
|
+
validated.push({
|
|
4035
|
+
role: "assistant",
|
|
4036
|
+
content: textBlocks.map((b) => b.text).join("\n")
|
|
4037
|
+
});
|
|
4038
|
+
continue;
|
|
4039
|
+
} else if (validToolUses.length > 0) {
|
|
4040
|
+
validated.push({
|
|
4041
|
+
role: "assistant",
|
|
4042
|
+
content: [...textBlocks, ...validToolUses]
|
|
4043
|
+
});
|
|
4044
|
+
continue;
|
|
4045
|
+
}
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
validated.push(msg);
|
|
4051
|
+
}
|
|
4052
|
+
return validated;
|
|
4053
|
+
}
|
|
4054
|
+
};
|
|
4055
|
+
var llm = new LLMClient();
|
|
4056
|
+
|
|
4057
|
+
// src/core/session.ts
|
|
4058
|
+
var SessionManager = class {
|
|
4059
|
+
startTime = Date.now();
|
|
4060
|
+
/**
|
|
4061
|
+
* Save current session state
|
|
4062
|
+
* (V13: Mostly a no-op as state is continuously saved to SQLite)
|
|
4063
|
+
*/
|
|
4064
|
+
async save() {
|
|
4065
|
+
const ctx = context.get();
|
|
4066
|
+
await context.save();
|
|
4067
|
+
return { sessionId: ctx.session_id, path: "sqlite" };
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* List all saved sessions
|
|
4071
|
+
*/
|
|
4072
|
+
async list() {
|
|
4073
|
+
try {
|
|
4074
|
+
const cfg = await config.load();
|
|
4075
|
+
const rows = db.getDb().prepare(`
|
|
4076
|
+
SELECT id, created_at, workspace
|
|
4077
|
+
FROM sessions
|
|
4078
|
+
ORDER BY created_at DESC
|
|
4079
|
+
`).all();
|
|
4080
|
+
const sessions = [];
|
|
4081
|
+
for (const row of rows) {
|
|
4082
|
+
const taskRow = db.getDb().prepare(`
|
|
4083
|
+
SELECT title FROM tasks
|
|
4084
|
+
WHERE session_id = ?
|
|
4085
|
+
ORDER BY created_at DESC LIMIT 1
|
|
4086
|
+
`).get(row.id);
|
|
4087
|
+
const modCount = db.getDb().prepare(`
|
|
4088
|
+
SELECT COUNT(*) as count FROM working_set WHERE session_id = ?
|
|
4089
|
+
`).get(row.id);
|
|
4090
|
+
sessions.push({
|
|
4091
|
+
id: row.id,
|
|
4092
|
+
savedAt: new Date(row.created_at).toISOString(),
|
|
4093
|
+
task: taskRow ? taskRow.title : null,
|
|
4094
|
+
filesModified: modCount ? modCount.count : 0,
|
|
4095
|
+
workspace: row.workspace || cfg.workspaceRoot
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
return sessions;
|
|
4099
|
+
} catch {
|
|
4100
|
+
return [];
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
/**
|
|
4104
|
+
* Delete a saved session and all related data
|
|
4105
|
+
*/
|
|
4106
|
+
async delete(sessionId) {
|
|
4107
|
+
try {
|
|
4108
|
+
const transaction = db.getDb().transaction(() => {
|
|
4109
|
+
db.getDb().prepare("DELETE FROM subtasks WHERE task_id IN (SELECT id FROM tasks WHERE session_id = ?)").run(sessionId);
|
|
4110
|
+
db.getDb().prepare("DELETE FROM tasks WHERE session_id = ?").run(sessionId);
|
|
4111
|
+
db.getDb().prepare("DELETE FROM working_set WHERE session_id = ?").run(sessionId);
|
|
4112
|
+
db.getDb().prepare("DELETE FROM usage_stats WHERE session_id = ?").run(sessionId);
|
|
4113
|
+
db.getDb().prepare("DELETE FROM events WHERE session_id = ?").run(sessionId);
|
|
4114
|
+
db.getDb().prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
|
|
4115
|
+
});
|
|
4116
|
+
transaction();
|
|
4117
|
+
return true;
|
|
4118
|
+
} catch (e) {
|
|
4119
|
+
console.error("Failed to delete session:", e);
|
|
4120
|
+
return false;
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
/**
|
|
4124
|
+
* Restore a saved session
|
|
4125
|
+
*/
|
|
4126
|
+
async restore(sessionId) {
|
|
4127
|
+
const sessionRow = db.getDb().prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
4128
|
+
if (!sessionRow) {
|
|
4129
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
4130
|
+
}
|
|
4131
|
+
await context.load(sessionId);
|
|
4132
|
+
const { bus: bus2 } = await import("./bus-N5GVOUS7.js");
|
|
4133
|
+
const events = await history.load();
|
|
4134
|
+
const filteredEvents = events.filter(
|
|
4135
|
+
(e) => e.type !== "approval_request" && e.type !== "choice_request"
|
|
4136
|
+
);
|
|
4137
|
+
bus2.emitAgent({ type: "restore_history", events: filteredEvents });
|
|
4138
|
+
await tasks.init();
|
|
4139
|
+
const stats = db.getDb().prepare(`
|
|
4140
|
+
SELECT
|
|
4141
|
+
SUM(cost) as sessionCost,
|
|
4142
|
+
SUM(input_tokens) as sessionInputTokens,
|
|
4143
|
+
SUM(output_tokens) as sessionOutputTokens,
|
|
4144
|
+
MIN(timestamp) as firstLog,
|
|
4145
|
+
MAX(timestamp) as lastLog
|
|
4146
|
+
FROM usage_stats
|
|
4147
|
+
WHERE session_id = ?
|
|
4148
|
+
`).get(sessionId);
|
|
4149
|
+
const duration = stats && stats.lastLog && stats.firstLog ? stats.lastLog - stats.firstLog : 0;
|
|
4150
|
+
usage.restoreSessionState({
|
|
4151
|
+
cost: stats ? stats.sessionCost || 0 : 0,
|
|
4152
|
+
inputTokens: stats ? stats.sessionInputTokens || 0 : 0,
|
|
4153
|
+
outputTokens: stats ? stats.sessionOutputTokens || 0 : 0,
|
|
4154
|
+
cacheReadTokens: 0,
|
|
4155
|
+
// Not currently tracked in usage_stats table separate?
|
|
4156
|
+
cacheCreationTokens: 0,
|
|
4157
|
+
duration
|
|
4158
|
+
});
|
|
4159
|
+
this.resetStartTime();
|
|
4160
|
+
this.startTime = Date.now() - duration;
|
|
4161
|
+
const row = sessionRow;
|
|
4162
|
+
if (row.llm_history) {
|
|
4163
|
+
try {
|
|
4164
|
+
const history2 = JSON.parse(row.llm_history);
|
|
4165
|
+
llm.restoreHistory(history2);
|
|
4166
|
+
} catch (e) {
|
|
4167
|
+
console.error("Failed to parse LLM history:", e);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
return { success: true };
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Generate session summary for shutdown
|
|
4174
|
+
*/
|
|
4175
|
+
async getSummary() {
|
|
4176
|
+
const ctx = context.get();
|
|
4177
|
+
const task = tasks.get();
|
|
4178
|
+
let tasksCompleted = 0;
|
|
4179
|
+
let tasksPending = 0;
|
|
4180
|
+
if (task) {
|
|
4181
|
+
for (const subtask of task.subtasks) {
|
|
4182
|
+
if (subtask.done) tasksCompleted++;
|
|
4183
|
+
else tasksPending++;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
return {
|
|
4187
|
+
sessionId: ctx.session_id,
|
|
4188
|
+
duration: Date.now() - this.startTime,
|
|
4189
|
+
filesRead: ctx.files_read.length,
|
|
4190
|
+
filesModified: ctx.files_modified.length,
|
|
4191
|
+
tasksCompleted,
|
|
4192
|
+
tasksPending,
|
|
4193
|
+
totalCost: usage.getSessionCost()
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
4196
|
+
resetStartTime() {
|
|
4197
|
+
this.startTime = Date.now();
|
|
4198
|
+
}
|
|
4199
|
+
getDuration() {
|
|
4200
|
+
return Date.now() - this.startTime;
|
|
4201
|
+
}
|
|
4202
|
+
formatDuration(ms) {
|
|
4203
|
+
const seconds = Math.floor(ms / 1e3);
|
|
4204
|
+
const minutes = Math.floor(seconds / 60);
|
|
4205
|
+
const hours = Math.floor(minutes / 60);
|
|
4206
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
4207
|
+
else if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
4208
|
+
else return `${seconds}s`;
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
var session = new SessionManager();
|
|
4212
|
+
|
|
4213
|
+
export {
|
|
4214
|
+
usage,
|
|
4215
|
+
auditor,
|
|
4216
|
+
sandbox,
|
|
4217
|
+
tasks,
|
|
4218
|
+
undo,
|
|
4219
|
+
diffManager,
|
|
4220
|
+
mcp,
|
|
4221
|
+
listRegistry,
|
|
4222
|
+
tools,
|
|
4223
|
+
history,
|
|
4224
|
+
llm,
|
|
4225
|
+
session
|
|
4226
|
+
};
|