@yeaft/webchat-agent 1.0.188 → 1.0.189

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/cli.js +12 -0
  2. package/index.js +10 -2
  3. package/local-run.js +218 -0
  4. package/local-runtime/server/.env.example +54 -0
  5. package/local-runtime/server/api.js +111 -0
  6. package/local-runtime/server/auth/aad.js +235 -0
  7. package/local-runtime/server/auth/login.js +156 -0
  8. package/local-runtime/server/auth/oauth-flow.js +277 -0
  9. package/local-runtime/server/auth/password-reset.js +134 -0
  10. package/local-runtime/server/auth/providers/alipay.js +125 -0
  11. package/local-runtime/server/auth/providers/github.js +82 -0
  12. package/local-runtime/server/auth/providers/google.js +60 -0
  13. package/local-runtime/server/auth/providers/microsoft.js +71 -0
  14. package/local-runtime/server/auth/providers/types.js +57 -0
  15. package/local-runtime/server/auth/providers/wechat.js +68 -0
  16. package/local-runtime/server/auth/register.js +91 -0
  17. package/local-runtime/server/auth/session-store.js +57 -0
  18. package/local-runtime/server/auth/token.js +85 -0
  19. package/local-runtime/server/auth/totp-auth.js +133 -0
  20. package/local-runtime/server/auth/utils.js +42 -0
  21. package/local-runtime/server/auth.js +8 -0
  22. package/local-runtime/server/check-node-version.js +74 -0
  23. package/local-runtime/server/config.js +298 -0
  24. package/local-runtime/server/context.js +140 -0
  25. package/local-runtime/server/create-user.js +59 -0
  26. package/local-runtime/server/database.js +12 -0
  27. package/local-runtime/server/db/connection.js +963 -0
  28. package/local-runtime/server/db/expert-db.js +171 -0
  29. package/local-runtime/server/db/identity-db.js +92 -0
  30. package/local-runtime/server/db/invitation-db.js +38 -0
  31. package/local-runtime/server/db/message-db.js +257 -0
  32. package/local-runtime/server/db/session-db.js +118 -0
  33. package/local-runtime/server/db/user-db.js +165 -0
  34. package/local-runtime/server/db/user-stats-db.js +185 -0
  35. package/local-runtime/server/db/yeaft-session-db.js +258 -0
  36. package/local-runtime/server/email.js +96 -0
  37. package/local-runtime/server/encryption.js +105 -0
  38. package/local-runtime/server/handlers/agent-conversation.js +347 -0
  39. package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
  40. package/local-runtime/server/handlers/agent-output.js +854 -0
  41. package/local-runtime/server/handlers/agent-sync.js +399 -0
  42. package/local-runtime/server/handlers/agent-work-center.js +27 -0
  43. package/local-runtime/server/handlers/client-conversation.js +1182 -0
  44. package/local-runtime/server/handlers/client-misc.js +254 -0
  45. package/local-runtime/server/handlers/client-work-center.js +269 -0
  46. package/local-runtime/server/handlers/client-workbench.js +146 -0
  47. package/local-runtime/server/handlers/session-pin-router.js +61 -0
  48. package/local-runtime/server/heartbeat-policy.js +46 -0
  49. package/local-runtime/server/index.js +275 -0
  50. package/local-runtime/server/package.json +55 -0
  51. package/local-runtime/server/perf-trace.js +154 -0
  52. package/local-runtime/server/proxy.js +273 -0
  53. package/local-runtime/server/routes/admin-routes.js +207 -0
  54. package/local-runtime/server/routes/auth-routes.js +322 -0
  55. package/local-runtime/server/routes/expert-routes.js +117 -0
  56. package/local-runtime/server/routes/invitation-routes.js +60 -0
  57. package/local-runtime/server/routes/session-routes.js +112 -0
  58. package/local-runtime/server/routes/upload-routes.js +109 -0
  59. package/local-runtime/server/routes/user-routes.js +241 -0
  60. package/local-runtime/server/totp.js +74 -0
  61. package/local-runtime/server/work-item-attachment-policy.js +56 -0
  62. package/local-runtime/server/ws-agent.js +319 -0
  63. package/local-runtime/server/ws-client.js +214 -0
  64. package/local-runtime/server/ws-utils.js +394 -0
  65. package/local-runtime/server/yeaft-asset-store.js +339 -0
  66. package/local-runtime/version.json +1 -0
  67. package/local-runtime/web/app.bundle.js +7673 -0
  68. package/local-runtime/web/app.bundle.js.gz +0 -0
  69. package/local-runtime/web/assets/avatars/README.md +34 -0
  70. package/local-runtime/web/assets/avatars/ada.svg +1 -0
  71. package/local-runtime/web/assets/avatars/alan.svg +1 -0
  72. package/local-runtime/web/assets/avatars/alice.svg +1 -0
  73. package/local-runtime/web/assets/avatars/anders.svg +1 -0
  74. package/local-runtime/web/assets/avatars/bezos.svg +1 -0
  75. package/local-runtime/web/assets/avatars/borges.svg +1 -0
  76. package/local-runtime/web/assets/avatars/buffett.svg +1 -0
  77. package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
  78. package/local-runtime/web/assets/avatars/dalio.svg +1 -0
  79. package/local-runtime/web/assets/avatars/dieter.svg +1 -0
  80. package/local-runtime/web/assets/avatars/drucker.svg +1 -0
  81. package/local-runtime/web/assets/avatars/einstein.svg +1 -0
  82. package/local-runtime/web/assets/avatars/grace.svg +1 -0
  83. package/local-runtime/web/assets/avatars/harari.svg +1 -0
  84. package/local-runtime/web/assets/avatars/jung.svg +1 -0
  85. package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
  86. package/local-runtime/web/assets/avatars/ken.svg +1 -0
  87. package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
  88. package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
  89. package/local-runtime/web/assets/avatars/linus.svg +1 -0
  90. package/local-runtime/web/assets/avatars/luxun.svg +1 -0
  91. package/local-runtime/web/assets/avatars/margaret.svg +1 -0
  92. package/local-runtime/web/assets/avatars/martin.svg +1 -0
  93. package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
  94. package/local-runtime/web/assets/avatars/munger.svg +1 -0
  95. package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
  96. package/local-runtime/web/assets/avatars/norman.svg +1 -0
  97. package/local-runtime/web/assets/avatars/shannon.svg +1 -0
  98. package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
  99. package/local-runtime/web/assets/avatars/socrates.svg +1 -0
  100. package/local-runtime/web/assets/avatars/steve.svg +1 -0
  101. package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
  102. package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
  103. package/local-runtime/web/docx-preview.min.js +2 -0
  104. package/local-runtime/web/docx-preview.min.js.gz +0 -0
  105. package/local-runtime/web/html-to-image.min.js +2 -0
  106. package/local-runtime/web/html-to-image.min.js.gz +0 -0
  107. package/local-runtime/web/index.html +21 -0
  108. package/local-runtime/web/jszip.min.js +13 -0
  109. package/local-runtime/web/jszip.min.js.gz +0 -0
  110. package/local-runtime/web/mermaid.min.js +2843 -0
  111. package/local-runtime/web/mermaid.min.js.gz +0 -0
  112. package/local-runtime/web/msal-browser.min.js +69 -0
  113. package/local-runtime/web/msal-browser.min.js.gz +0 -0
  114. package/local-runtime/web/style.bundle.css +10 -0
  115. package/local-runtime/web/style.bundle.css.gz +0 -0
  116. package/local-runtime/web/vendor.bundle.js +1522 -0
  117. package/local-runtime/web/vendor.bundle.js.gz +0 -0
  118. package/local-runtime/web/xlsx.min.js +24 -0
  119. package/local-runtime/web/xlsx.min.js.gz +0 -0
  120. package/package.json +13 -3
  121. package/scripts/prepare-local-runtime.js +25 -0
@@ -0,0 +1,394 @@
1
+ import { WebSocket } from 'ws';
2
+ import { CONFIG } from './config.js';
3
+ import { encrypt, decrypt, isEncrypted, encodeKey } from './encryption.js';
4
+ import { sessionDb } from './database.js';
5
+ import { agents, webClients, directoryCache, DIR_CACHE_TTL, DIR_CACHE_MAX_SIZE, trackMessageBytesSent } from './context.js';
6
+
7
+ // Send message to web client.
8
+ // feat-ws-plaintext-negotiation: defaults to plaintext when the client
9
+ // advertised plaintext-ok capability via client_hello (encryptOutbound
10
+ // flipped to false). Falls back to ciphertext for old clients that
11
+ // haven't sent client_hello — back-compat. skipAuth still forces
12
+ // plaintext for local dev visibility.
13
+ export async function sendToWebClient(client, msg) {
14
+ if (client.ws.readyState !== WebSocket.OPEN) return;
15
+
16
+ // Plaintext path: dev mode OR new client that announced plaintext-ok.
17
+ if (CONFIG.skipAuth || client.encryptOutbound === false) {
18
+ const payload = JSON.stringify(msg);
19
+ trackMessageBytesSent(client.userId, payload.length, msg.type);
20
+ client.ws.send(payload);
21
+ return;
22
+ }
23
+
24
+ // Encrypted fallback for old clients that never sent client_hello.
25
+ if (!client.sessionKey) {
26
+ console.error('Cannot send to client: missing session key in production mode');
27
+ client.ws.close(1008, 'Encryption required');
28
+ return;
29
+ }
30
+ try {
31
+ const encrypted = await encrypt(msg, client.sessionKey);
32
+ const payload = JSON.stringify(encrypted);
33
+ trackMessageBytesSent(client.userId, payload.length, msg.type);
34
+ if (msg.type === 'file_content') console.log(`[sendToWebClient] file_content encrypted, payload size=${payload.length}, compressed=${encrypted.z}`);
35
+ client.ws.send(payload);
36
+ } catch (e) {
37
+ console.error(`[sendToWebClient] Error encrypting/sending ${msg.type}:`, e.message, e.stack);
38
+ }
39
+ }
40
+
41
+ // Send message to agent.
42
+ // feat-ws-plaintext-negotiation: same shape as sendToWebClient — defaults
43
+ // to plaintext when the agent advertised the `plaintext-ok` capability.
44
+ // Falls back to ciphertext for old agents that haven't.
45
+ export async function sendToAgent(agent, msg) {
46
+ if (agent.ws.readyState !== WebSocket.OPEN) return;
47
+
48
+ if (CONFIG.skipAuth || agent.encryptOutbound === false) {
49
+ agent.ws.send(JSON.stringify(msg));
50
+ return;
51
+ }
52
+
53
+ if (!agent.sessionKey) {
54
+ console.error('Cannot send to agent: missing session key in production mode');
55
+ agent.ws.close(1008, 'Encryption required');
56
+ return;
57
+ }
58
+ const encrypted = await encrypt(msg, agent.sessionKey);
59
+ agent.ws.send(JSON.stringify(encrypted));
60
+ }
61
+
62
+ // Parse incoming message (decrypt if needed)
63
+ export async function parseMessage(data, sessionKey) {
64
+ try {
65
+ const parsed = JSON.parse(data.toString());
66
+
67
+ if (sessionKey && isEncrypted(parsed)) {
68
+ return await decrypt(parsed, sessionKey);
69
+ }
70
+
71
+ return parsed;
72
+ } catch (e) {
73
+ console.error('Failed to parse message:', e.message);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // 广播 agent 列表给所有已认证的 web 客户端(按 owner 过滤)
79
+ export async function broadcastAgentList() {
80
+ for (const [, client] of webClients) {
81
+ if (client.authenticated) {
82
+ // 按 owner 过滤:只显示属于该用户的 agent,或 ownerId=null 的全局 agent(仅 admin 可见)
83
+ const agentList = Array.from(agents.entries())
84
+ .filter(([, agent]) =>
85
+ CONFIG.skipAuth ||
86
+ agent.ownerId === client.userId ||
87
+ (!agent.ownerId && client.role === 'admin')
88
+ )
89
+ .map(([id, agent]) => ({
90
+ id,
91
+ name: agent.name,
92
+ instanceId: agent.instanceId || null,
93
+ workDir: agent.workDir,
94
+ online: agent.ws.readyState === WebSocket.OPEN,
95
+ status: agent.status || 'ready',
96
+ latency: agent.latency || null,
97
+ capabilities: agent.capabilities || ['terminal', 'file_editor', 'background_tasks'],
98
+ version: agent.version || null,
99
+ yeaftStatus: agent.yeaftStatus || null,
100
+ proxyPorts: agent.proxyPorts || [],
101
+ conversations: Array.from(agent.conversations.values()).filter(c =>
102
+ CONFIG.skipAuth || !c.userId || c.userId === client.userId
103
+ ).map(c => {
104
+ // fix-chat-title-sticky: lazy-hydrate `customTitle` from
105
+ // the DB whenever it's missing, so pre-fix in-memory
106
+ // convInfo objects (and any future rebuild path that
107
+ // forgets) still produce a correct broadcast.
108
+ if (!c.title || c.customTitle === undefined) {
109
+ const dbSession = sessionDb.get(c.id);
110
+ if (dbSession?.title) {
111
+ c.title = c.title || dbSession.title;
112
+ }
113
+ if (dbSession) {
114
+ c.pinned = !!dbSession.is_pinned;
115
+ if (c.customTitle === undefined) c.customTitle = !!dbSession.customTitle;
116
+ }
117
+ } else {
118
+ const dbSession = sessionDb.get(c.id);
119
+ if (dbSession) c.pinned = !!dbSession.is_pinned;
120
+ }
121
+ return c;
122
+ })
123
+ }));
124
+ await sendToWebClient(client, {
125
+ type: 'agent_list',
126
+ agents: agentList
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ // 发送 conversation 列表给特定客户端
133
+ export async function sendConversationList(clientId, agentId) {
134
+ const client = webClients.get(clientId);
135
+ const agent = agents.get(agentId);
136
+
137
+ if (client && agent) {
138
+ const filteredConvs = Array.from(agent.conversations.values()).filter(c =>
139
+ CONFIG.skipAuth || !c.userId || c.userId === client.userId
140
+ ).map(c => {
141
+ // fix-chat-title-sticky: same lazy-hydrate as broadcastAgentList —
142
+ // ensure `customTitle` is reliably set on the wire so the
143
+ // per-message auto-title gate stays honest after restart / sync.
144
+ if (!c.title || c.customTitle === undefined) {
145
+ const dbSession = sessionDb.get(c.id);
146
+ if (dbSession?.title) {
147
+ c.title = c.title || dbSession.title;
148
+ }
149
+ if (dbSession) {
150
+ c.pinned = !!dbSession.is_pinned;
151
+ if (c.customTitle === undefined) c.customTitle = !!dbSession.customTitle;
152
+ }
153
+ } else {
154
+ const dbSession = sessionDb.get(c.id);
155
+ if (dbSession) c.pinned = !!dbSession.is_pinned;
156
+ }
157
+ return c;
158
+ });
159
+ await sendToWebClient(client, {
160
+ type: 'conversation_list',
161
+ agentId,
162
+ conversations: filteredConvs
163
+ });
164
+ }
165
+ }
166
+
167
+ // 通知会话更新给相关客户端
168
+ export async function notifyConversationUpdate(agentId, msg) {
169
+ // 对于 folders_list、history_sessions_list,优先定向发送给请求者
170
+ if (msg.type === 'folders_list' || msg.type === 'history_sessions_list' || msg.type === 'models_list') {
171
+ const targetClientId = msg._requestClientId;
172
+ if (targetClientId) {
173
+ const targetClient = webClients.get(targetClientId);
174
+ if (targetClient?.authenticated) {
175
+ const { _requestClientId, ...cleanMsg } = msg;
176
+ await sendToWebClient(targetClient, cleanMsg);
177
+ return;
178
+ }
179
+ // Target client disconnected/reconnected — fall through to broadcast
180
+ }
181
+ // Fallback: 如果没有 _requestClientId 或 target 不可用,发送给所有已认证客户端
182
+ const { _requestClientId: _, ...cleanMsg } = msg;
183
+ for (const [, client] of webClients) {
184
+ if (client.authenticated) {
185
+ await sendToWebClient(client, cleanMsg);
186
+ }
187
+ }
188
+ return;
189
+ }
190
+
191
+ // 对于 conversation_created 和 conversation_resumed,只发送给拥有该会话的用户的客户端
192
+ // 避免其他用户收到后误触发 select_conversation 导致 Permission denied
193
+ if (msg.type === 'conversation_created' || msg.type === 'conversation_resumed') {
194
+ const msgWithAgent = { ...msg, agentId };
195
+ const ownerId = msg.userId;
196
+ for (const [, client] of webClients) {
197
+ // 只发送给已认证且属于同一用户的客户端(或开发模式下所有客户端)
198
+ if (client.authenticated && (!ownerId || CONFIG.skipAuth || client.userId === ownerId)) {
199
+ await sendToWebClient(client, msgWithAgent);
200
+ }
201
+ }
202
+ return;
203
+ }
204
+
205
+ for (const [, client] of webClients) {
206
+ if (client.authenticated && client.currentAgent === agentId) {
207
+ await sendToWebClient(client, msg);
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * 检查用户是否有权访问指定的 Agent
214
+ * @param {string} agentId
215
+ * @param {string} userId
216
+ * @param {string} [role] - 用户角色,ownerId=null 的全局 agent 仅 admin 可访问
217
+ * @returns {boolean}
218
+ */
219
+ export function verifyAgentOwnership(agentId, userId, role = null) {
220
+ if (CONFIG.skipAuth) return true;
221
+ const agent = agents.get(agentId);
222
+ if (!agent) return false;
223
+ if (agent.ownerId === userId) return true;
224
+ // ownerId=null 表示通过全局 AGENT_SECRET 连接,仅 admin 可访问
225
+ if (!agent.ownerId && role === 'admin') return true;
226
+ return false;
227
+ }
228
+
229
+ export function resolveAgentAccessError(agentId, userId, role = null) {
230
+ if (!agentId) return 'Agent not found';
231
+ const agent = agents.get(agentId);
232
+ if (!agent) return 'Agent not found or offline';
233
+ if (!verifyAgentOwnership(agentId, userId, role)) return 'Agent access denied';
234
+ if (agent.ws?.readyState !== WebSocket.OPEN) return 'Agent not found or offline';
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * Broadcast an Agent-level event to every authenticated browser that may
240
+ * access the Agent. Unlike conversation forwarding, this also supports
241
+ * ownerless global Agents, which are visible only to admins.
242
+ */
243
+ export async function forwardAgentEvent(agentId, msg) {
244
+ for (const [, client] of webClients) {
245
+ if (!client.authenticated) continue;
246
+ if (!verifyAgentOwnership(agentId, client.userId, client.role)) continue;
247
+ await sendToWebClient(client, msg);
248
+ }
249
+ }
250
+
251
+ // 转发消息给拥有该会话的用户的所有客户端
252
+ export async function forwardToClients(agentId, conversationId, msg) {
253
+ // ★ Security: 从 agent.conversations 获取会话的 userId
254
+ const agent = agents.get(agentId);
255
+ const conv = agent?.conversations.get(conversationId);
256
+ // ★ Security: ownerId 优先级: conversation.userId > _requestUserId > agent.ownerId
257
+ // 确保生产模式下不会因 ownerId 缺失而向所有用户广播
258
+ const ownerId = conv?.userId || msg._requestUserId || agent?.ownerId || null;
259
+
260
+ let forwarded = false;
261
+ for (const [clientId, client] of webClients) {
262
+ // 只发送给已认证且属于同一用户的客户端
263
+ // 仅在开发模式下,无 ownerId 时允许广播给所有已认证客户端
264
+ if (client.authenticated && (CONFIG.skipAuth ? (!ownerId || client.userId === ownerId) : (ownerId && client.userId === ownerId))) {
265
+ try {
266
+ await sendToWebClient(client, msg);
267
+ forwarded = true;
268
+ if (msg.type === 'file_content') console.log(`[Forward] file_content sent to client ${clientId}, size=${JSON.stringify(msg).length}`);
269
+ } catch (e) {
270
+ console.error(`[Forward] Error sending ${msg.type} to client ${clientId}:`, e.message);
271
+ }
272
+ } else if (msg.type === 'file_content') {
273
+ console.log(`[Forward] file_content SKIPPED client ${clientId}: auth=${client.authenticated}, ownerId=${ownerId}, clientUserId=${client.userId}`);
274
+ }
275
+ }
276
+ if (!forwarded) {
277
+ if (msg.type === 'file_content') {
278
+ console.warn(`[Forward] file_content NOT forwarded! conv=${conversationId}, agent=${agentId}, ownerId=${ownerId}, _reqUser=${msg._requestUserId}, webClients=${webClients.size}`);
279
+ } else if (msg.type === 'claude_output') {
280
+ console.warn(`[Forward] No authenticated clients for conv=${conversationId}, owner=${ownerId}`);
281
+ } else if (msg.type === 'directory_listing') {
282
+ console.warn(`[Forward] directory_listing NOT forwarded! conv=${conversationId}, agent=${agentId}, ownerId=${ownerId}, _reqUser=${msg._requestUserId}, webClients=${webClients.size}`);
283
+ }
284
+ }
285
+ }
286
+
287
+ // 转发消息给指定 agent
288
+ export async function forwardToAgent(agentId, msg) {
289
+ const agent = agents.get(agentId);
290
+ if (agent) {
291
+ await sendToAgent(agent, msg);
292
+ }
293
+ }
294
+
295
+ // 通过 agent name 查找 agent
296
+ export function findAgentByName(agentName) {
297
+ for (const [id, agent] of agents) {
298
+ if (agent.name === agentName || id === agentName) {
299
+ return { id, agent };
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+
305
+ /**
306
+ * 检查用户是否拥有指定的会话
307
+ */
308
+ export function verifyConversationOwnership(conversationId, userId) {
309
+ if (!conversationId || !userId) {
310
+ console.warn(`[Ownership] Check failed: conversationId=${conversationId}, userId=${userId} (missing parameter)`);
311
+ return false;
312
+ }
313
+
314
+ // 先从内存中的 agent conversations 查找
315
+ for (const [agentId, agent] of agents) {
316
+ const conv = agent.conversations.get(conversationId);
317
+ if (conv) {
318
+ // 无 owner 的 conversation(创建时未启用 auth)允许任何已认证用户访问
319
+ if (!conv.userId) return true;
320
+ const match = conv.userId === userId;
321
+ if (!match) {
322
+ console.warn(`[Ownership] Mismatch for ${conversationId}: conv.userId=${conv.userId}, client.userId=${userId}, agent=${agentId}`);
323
+ }
324
+ return match;
325
+ }
326
+ }
327
+
328
+ // 再从数据库查找
329
+ try {
330
+ const session = sessionDb.get(conversationId);
331
+ if (session) {
332
+ if (!session.user_id) return true;
333
+ const match = session.user_id === userId;
334
+ if (!match) {
335
+ console.warn(`[Ownership] DB mismatch for ${conversationId}: session.user_id=${session.user_id}, client.userId=${userId}`);
336
+ }
337
+ return match;
338
+ }
339
+ } catch (e) {
340
+ console.error('Failed to verify conversation ownership:', e.message);
341
+ }
342
+
343
+ // conversation 在内存和数据库中都找不到 — 拒绝访问
344
+ console.warn(`[Ownership] Conversation ${conversationId} not found anywhere, userId=${userId}, denying access`);
345
+ return false;
346
+ }
347
+
348
+ // ★ Phase 4: Directory cache helpers
349
+
350
+ function normalizeDirPath(p) {
351
+ return (p || '').replace(/\\/g, '/').replace(/\/+$/, '');
352
+ }
353
+
354
+ function getDirCacheKey(agentId, dirPath) {
355
+ return `${agentId}:${normalizeDirPath(dirPath)}`;
356
+ }
357
+
358
+ export function getCachedDir(agentId, dirPath) {
359
+ const key = getDirCacheKey(agentId, dirPath);
360
+ const cached = directoryCache.get(key);
361
+ if (cached && Date.now() - cached.timestamp < DIR_CACHE_TTL) {
362
+ return cached.entries;
363
+ }
364
+ if (cached) directoryCache.delete(key);
365
+ return null;
366
+ }
367
+
368
+ export function setCachedDir(agentId, dirPath, entries) {
369
+ if (directoryCache.size >= DIR_CACHE_MAX_SIZE) {
370
+ const oldest = [...directoryCache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
371
+ if (oldest) directoryCache.delete(oldest[0]);
372
+ }
373
+ directoryCache.set(getDirCacheKey(agentId, dirPath), {
374
+ entries, timestamp: Date.now()
375
+ });
376
+ }
377
+
378
+ export function clearAgentDirCache(agentId) {
379
+ const prefix = `${agentId}:`;
380
+ const keysToDelete = [];
381
+ for (const key of directoryCache.keys()) {
382
+ if (key.startsWith(prefix)) keysToDelete.push(key);
383
+ }
384
+ for (const key of keysToDelete) directoryCache.delete(key);
385
+ }
386
+
387
+ export function invalidateParentDirCache(agentId, filePath) {
388
+ if (!filePath) return;
389
+ const parentDir = filePath.replace(/[\\\/][^\\\/]+$/, '');
390
+ if (parentDir) {
391
+ const key = getDirCacheKey(agentId, parentDir);
392
+ directoryCache.delete(key);
393
+ }
394
+ }