hyacinth-ai 0.9.8 → 0.9.10
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/dist/channels/builtin/http-webhook.js +32 -0
- package/dist/context/section-resolver.js +1 -1
- package/dist/gateway/factory.js +13 -2
- package/dist/gateway/tui.js +12 -0
- package/dist/hot-reload/tool-watcher.js +24 -6
- package/dist/knowledge/structured-store.js +2 -2
- package/dist/prompts/attention/attention.md +2 -0
- package/dist/prompts/persona/PartnerSoul.md +2 -1
- package/dist/setup/persona-bootstrap.d.ts +9 -0
- package/dist/setup/persona-bootstrap.js +52 -0
- package/null +0 -0
- package/package.json +4 -4
|
@@ -215,12 +215,18 @@ export class HttpWebhookChannel {
|
|
|
215
215
|
const { TuiWsSession } = await import('./tui-ws-session.js');
|
|
216
216
|
const { randomBytes } = await import('node:crypto');
|
|
217
217
|
const tuiWss = new WebSocketServer({ noServer: true });
|
|
218
|
+
const desktopWss = new WebSocketServer({ noServer: true });
|
|
218
219
|
this.app.server.on('upgrade', (request, socket, head) => {
|
|
219
220
|
if (request.url === '/tui') {
|
|
220
221
|
tuiWss.handleUpgrade(request, socket, head, (ws) => {
|
|
221
222
|
tuiWss.emit('connection', ws, request);
|
|
222
223
|
});
|
|
223
224
|
}
|
|
225
|
+
else if (request.url === '/desktop') {
|
|
226
|
+
desktopWss.handleUpgrade(request, socket, head, (ws) => {
|
|
227
|
+
desktopWss.emit('connection', ws, request);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
224
230
|
else {
|
|
225
231
|
socket.destroy();
|
|
226
232
|
}
|
|
@@ -247,6 +253,32 @@ export class HttpWebhookChannel {
|
|
|
247
253
|
logger.info('TUI WS client disconnected', { sessionId });
|
|
248
254
|
});
|
|
249
255
|
});
|
|
256
|
+
// ── Desktop GUI WebSocket 端点 ──
|
|
257
|
+
desktopWss.on('connection', (ws) => {
|
|
258
|
+
const now = new Date();
|
|
259
|
+
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
|
260
|
+
const time = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
|
261
|
+
const sessionId = `webui_${date}-${time}-${randomBytes(3).toString('hex')}`;
|
|
262
|
+
const session = new TuiWsSession(ws, sessionId);
|
|
263
|
+
logger.info('Desktop WS client connected', { sessionId });
|
|
264
|
+
if (this.wsAgentFactory) {
|
|
265
|
+
session.initialize(this.wsAgentFactory).catch((err) => {
|
|
266
|
+
logger.error('Desktop WS init failed', err instanceof Error ? err : new Error(String(err)));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
logger.warn('Desktop WS agentFactory not available');
|
|
271
|
+
}
|
|
272
|
+
ws.on('message', (data) => {
|
|
273
|
+
session.handleMessage(data).catch((err) => {
|
|
274
|
+
logger.error('Desktop WS msg error', err instanceof Error ? err : new Error(String(err)));
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
ws.on('close', () => {
|
|
278
|
+
session.close().catch(() => { });
|
|
279
|
+
logger.info('Desktop WS client disconnected', { sessionId });
|
|
280
|
+
});
|
|
281
|
+
});
|
|
250
282
|
// ── Start ───────────────────────────────────────────────────
|
|
251
283
|
await this.app.listen({ port: port, host: host });
|
|
252
284
|
this.status = 'active';
|
|
@@ -134,7 +134,7 @@ async function resolveRuntime(sec, ctx) {
|
|
|
134
134
|
// # currentDate 是系统元数据标记(非用户输入),模型训练数据中识别为背景信息
|
|
135
135
|
const [datePart, timePart] = ctx.timestamp.split(' ');
|
|
136
136
|
const dateSlash = datePart.replace(/-/g, '/');
|
|
137
|
-
return `# currentDate\
|
|
137
|
+
return `# currentDate\n(系统提供) Today is ${dateSlash}, ${timePart}.`;
|
|
138
138
|
}
|
|
139
139
|
if (src === 'runtime:userInput') {
|
|
140
140
|
return ctx.userInput || undefined;
|
package/dist/gateway/factory.js
CHANGED
|
@@ -27,7 +27,7 @@ import { MemoryStore } from '../memory/memory-store.js';
|
|
|
27
27
|
import { AgentLoop } from '../orchestrator/loop.js';
|
|
28
28
|
import { PluginManager } from '../plugins/index.js';
|
|
29
29
|
import { initDependencyAnalyzer } from '../dependency/index.js';
|
|
30
|
-
import { ensureGlobalPersonaFiles, getBootstrapStatus } from '../setup/persona-bootstrap.js';
|
|
30
|
+
import { ensureGlobalPersonaFiles, ensureGlobalPromptDir, getBootstrapStatus } from '../setup/persona-bootstrap.js';
|
|
31
31
|
import { ConfigManager } from '../setup/config.js';
|
|
32
32
|
import { RuntimeConfigCenter } from '../runtime/config-center.js';
|
|
33
33
|
import { getDefaultConfig } from '../runtime/defaults.js';
|
|
@@ -119,6 +119,17 @@ export async function createAgent(options, supervisor) {
|
|
|
119
119
|
const personaSetup = await ensureGlobalPersonaFiles();
|
|
120
120
|
const effectivePersonaDir = personaDir ?? personaSetup.personaDir;
|
|
121
121
|
const effectiveBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(effectivePersonaDir));
|
|
122
|
+
// ── 内置 Prompt 同步 ─────────────────────────────────────────────
|
|
123
|
+
// 将所有内置 prompt 同步到 ~/.agent/prompts/ 下,
|
|
124
|
+
// 使得 loadPrompt() 的查找优先级正确:外部覆盖 > 内置兜底。
|
|
125
|
+
await ensureGlobalPromptDir('attention');
|
|
126
|
+
await ensureGlobalPromptDir('tools');
|
|
127
|
+
await ensureGlobalPromptDir('agents');
|
|
128
|
+
await ensureGlobalPromptDir('flows');
|
|
129
|
+
await ensureGlobalPromptDir('skills');
|
|
130
|
+
await ensureGlobalPromptDir('precise');
|
|
131
|
+
await ensureGlobalPromptDir('environment');
|
|
132
|
+
// root 级文件 summary.md 由 loadPrompt 递归搜索找到,暂不单独同步
|
|
122
133
|
// ── 注册 Flow(Bootstrap + TODO + Spec)──────────────────────────
|
|
123
134
|
flowRegistry.register(new BootstrapFlow(effectivePersonaDir));
|
|
124
135
|
flowRegistry.register(new TodoFlow());
|
|
@@ -255,7 +266,7 @@ export async function createAgent(options, supervisor) {
|
|
|
255
266
|
description: '会话临时工具',
|
|
256
267
|
getContent: () => {
|
|
257
268
|
const names = toolRegistry.getHotAddedNames();
|
|
258
|
-
return names.length > 0 ? `[
|
|
269
|
+
return names.length > 0 ? `[Hot-added tools (session-scoped, additional to standard tools)]\n${names.join(', ')}` : '';
|
|
259
270
|
},
|
|
260
271
|
});
|
|
261
272
|
const channelRegistry = new ModelChannelRegistry(cwd);
|
package/dist/gateway/tui.js
CHANGED
|
@@ -1415,6 +1415,10 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
1415
1415
|
// 就地切换,不重启
|
|
1416
1416
|
await loop.switchSession(dir);
|
|
1417
1417
|
sessionDir = dir;
|
|
1418
|
+
lastTurnCount = 0;
|
|
1419
|
+
lastTokensUsed = 0;
|
|
1420
|
+
const newStats = await statsManager.get(dir);
|
|
1421
|
+
refreshStatus(loop.getTurnInfo(newStats.turn_count ?? 0, newStats.current_context_tokens ?? 0));
|
|
1418
1422
|
chatLog.clearAll();
|
|
1419
1423
|
replayEvents(chatLog, dir);
|
|
1420
1424
|
chatLog.addSystem(theme.success(`已切换到 session ${sessionId}`));
|
|
@@ -2460,6 +2464,10 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
2460
2464
|
await loop.switchSession(newSessionDir);
|
|
2461
2465
|
sessionDir = newSessionDir;
|
|
2462
2466
|
preciseModeActive = true;
|
|
2467
|
+
lastTurnCount = 0;
|
|
2468
|
+
lastTokensUsed = 0;
|
|
2469
|
+
const psStats = await statsManager.get(newSessionDir);
|
|
2470
|
+
refreshStatus(loop.getTurnInfo(psStats.turn_count ?? 0, psStats.current_context_tokens ?? 0));
|
|
2463
2471
|
const label = existingPrecise ? '恢复已有' : '新建';
|
|
2464
2472
|
chatLog.addSystem(theme.success(`精确模式已开启 — ${label} session: ${newSession.id}`));
|
|
2465
2473
|
chatLog.clearAll();
|
|
@@ -2478,6 +2486,10 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
2478
2486
|
await loop.switchSession(originalSessionDir);
|
|
2479
2487
|
sessionDir = originalSessionDir;
|
|
2480
2488
|
originalSessionDir = null;
|
|
2489
|
+
lastTurnCount = 0;
|
|
2490
|
+
lastTokensUsed = 0;
|
|
2491
|
+
const origStats = await statsManager.get(sessionDir);
|
|
2492
|
+
refreshStatus(loop.getTurnInfo(origStats.turn_count ?? 0, origStats.current_context_tokens ?? 0));
|
|
2481
2493
|
chatLog.clearAll();
|
|
2482
2494
|
replayEvents(chatLog, sessionDir);
|
|
2483
2495
|
}
|
|
@@ -20,8 +20,9 @@ export function watchTools(deps) {
|
|
|
20
20
|
}
|
|
21
21
|
catch { /* ignore */ }
|
|
22
22
|
const fileMap = new Map();
|
|
23
|
+
const mtimeMap = new Map();
|
|
23
24
|
// 初始扫描不标记 hotAdded — 工具应进入 Zone 2 tool_rules
|
|
24
|
-
scanAndSync(toolsDir, toolRegistry, fileMap, true);
|
|
25
|
+
scanAndSync(toolsDir, toolRegistry, fileMap, mtimeMap, true);
|
|
25
26
|
let debounceTimer = null;
|
|
26
27
|
logger.info(`Watching tools directory: ${toolsDir}`);
|
|
27
28
|
const watcher = fs.watch(toolsDir, { recursive: true }, (_eventType, filename) => {
|
|
@@ -32,20 +33,32 @@ export function watchTools(deps) {
|
|
|
32
33
|
if (debounceTimer)
|
|
33
34
|
clearTimeout(debounceTimer);
|
|
34
35
|
// 文件变更触发的重新扫描 — 此时才标记 hotAdded,工具进 Zone 5 session-tools
|
|
35
|
-
debounceTimer = setTimeout(() => scanAndSync(toolsDir, toolRegistry, fileMap, false), debounceMs);
|
|
36
|
+
debounceTimer = setTimeout(() => scanAndSync(toolsDir, toolRegistry, fileMap, mtimeMap, false), debounceMs);
|
|
36
37
|
});
|
|
37
38
|
watcher.on('error', (err) => {
|
|
38
39
|
logger.warn(`Tool watcher error: ${err.message}`, { error: err.message });
|
|
39
40
|
});
|
|
40
41
|
return watcher;
|
|
41
42
|
}
|
|
42
|
-
async function scanAndSync(dir, registry, fileMap, isInitialScan) {
|
|
43
|
+
async function scanAndSync(dir, registry, fileMap, mtimeMap, isInitialScan) {
|
|
43
44
|
const resolved = resolveToolEntries(dir);
|
|
44
45
|
const currentKeys = new Set(resolved.map((r) => r.trackingKey));
|
|
45
46
|
for (const entry of resolved) {
|
|
46
47
|
const knownToolName = fileMap.get(entry.trackingKey);
|
|
47
48
|
if (knownToolName !== undefined) {
|
|
48
|
-
// 已跟踪 →
|
|
49
|
+
// 已跟踪 → 检查文件是否真的变了,避免 fs.watch 误触发导致全部标记为 hot-added
|
|
50
|
+
let mtime;
|
|
51
|
+
try {
|
|
52
|
+
mtime = fs.statSync(entry.filePath).mtimeMs;
|
|
53
|
+
}
|
|
54
|
+
catch { /* stat 失败则走重载 */ }
|
|
55
|
+
if (mtime !== undefined) {
|
|
56
|
+
const prevMtime = mtimeMap.get(entry.trackingKey);
|
|
57
|
+
if (prevMtime === mtime)
|
|
58
|
+
continue; // 文件没变,跳过
|
|
59
|
+
mtimeMap.set(entry.trackingKey, mtime);
|
|
60
|
+
}
|
|
61
|
+
// 重新加载(文件变更触发的热重载)
|
|
49
62
|
if (entry.factory) {
|
|
50
63
|
// Python 工具:不再解析
|
|
51
64
|
const tool = entry.factory();
|
|
@@ -79,13 +92,14 @@ async function scanAndSync(dir, registry, fileMap, isInitialScan) {
|
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
else {
|
|
82
|
-
await loadAndRegister(entry, registry, fileMap, isInitialScan);
|
|
95
|
+
await loadAndRegister(entry, registry, fileMap, mtimeMap, isInitialScan);
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
for (const [key, toolName] of fileMap.entries()) {
|
|
86
99
|
if (!currentKeys.has(key)) {
|
|
87
100
|
registry.unregister(toolName);
|
|
88
101
|
fileMap.delete(key);
|
|
102
|
+
mtimeMap.delete(key);
|
|
89
103
|
logger.info(`Tool unregistered: ${toolName} (${key})`);
|
|
90
104
|
}
|
|
91
105
|
}
|
|
@@ -138,7 +152,7 @@ function resolveToolEntries(dir) {
|
|
|
138
152
|
}
|
|
139
153
|
return entries;
|
|
140
154
|
}
|
|
141
|
-
async function loadAndRegister(entry, registry, fileMap, isInitialScan) {
|
|
155
|
+
async function loadAndRegister(entry, registry, fileMap, mtimeMap, isInitialScan) {
|
|
142
156
|
try {
|
|
143
157
|
let tool;
|
|
144
158
|
if (entry.factory) {
|
|
@@ -159,6 +173,10 @@ async function loadAndRegister(entry, registry, fileMap, isInitialScan) {
|
|
|
159
173
|
if (!isInitialScan)
|
|
160
174
|
registry.markHotAdded(tool.name);
|
|
161
175
|
fileMap.set(entry.trackingKey, tool.name);
|
|
176
|
+
try {
|
|
177
|
+
mtimeMap.set(entry.trackingKey, fs.statSync(entry.filePath).mtimeMs);
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
162
180
|
logger.info(`Tool registered: ${tool.name} (${entry.label})`);
|
|
163
181
|
}
|
|
164
182
|
catch (err) {
|
|
@@ -165,7 +165,7 @@ export class StructuredStore {
|
|
|
165
165
|
score += Math.round(3 * weight);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
-
if (
|
|
168
|
+
if (matchedTags.length > 0) {
|
|
169
169
|
results.push({ entry, score, matchedTags });
|
|
170
170
|
}
|
|
171
171
|
}
|
|
@@ -237,7 +237,7 @@ export class StructuredStore {
|
|
|
237
237
|
const refs = results.slice(maxMain, maxMain + maxRefs);
|
|
238
238
|
for (const r of main) {
|
|
239
239
|
const tags = r.matchedTags.length > 0 ? `匹配标签: ${r.matchedTags.join(', ')}` : 'FTS5 匹配';
|
|
240
|
-
lines.push(
|
|
240
|
+
lines.push(`(知识库提供) ━━ ${r.entry.id} ── [${r.entry.category}] ── ${tags}`);
|
|
241
241
|
lines.push(r.entry.content);
|
|
242
242
|
if (r.entry.ctx_before)
|
|
243
243
|
lines.push(`前置: ${r.entry.ctx_before}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
注意事项:
|
|
2
2
|
- 减少不必要的对话轮次:能并行的独立工具调用一次发出,不要逐个等待
|
|
3
|
+
- 子Agent 注册一次即可反复 delegate,不要重复 create_sub_agent。
|
|
3
4
|
- 修改代码前先阅读相关代码,理解上下文
|
|
4
5
|
- 优先使用现有工具和函数,避免重复实现
|
|
5
6
|
- 执行命令时注意安全性,避免破坏性操作
|
|
@@ -10,3 +11,4 @@
|
|
|
10
11
|
- 复杂任务可交由子agent去完成,和验收,你统领全局
|
|
11
12
|
- 回复干净简洁,策划阶段不输出代码,具体执行再输出代码
|
|
12
13
|
- 可以使用各种skill来协助,子智能体你可以在编排提示词阶段之间传入skill
|
|
14
|
+
- 动手改任何代码之前必须先向用户询问并确认,改代码过程中如果出现不确定的也要问
|
|
@@ -19,6 +19,15 @@ export interface PersonaValidationResult {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function getGlobalPersonaDir(configHome?: string): string;
|
|
21
21
|
export declare const DEFAULT_PERSONA_DIR: string;
|
|
22
|
+
/**
|
|
23
|
+
* 确保内置 prompt 目录同步到 ~/.agent/prompts/{name}/。
|
|
24
|
+
*
|
|
25
|
+
* 和 persona 不同(有 bootstrap 状态机),这只是一个简单的文件复制:
|
|
26
|
+
* 首次安装时复制,已存在则跳过。
|
|
27
|
+
*
|
|
28
|
+
* 用于 attention.md 等非 persona 的内置 prompt 文件。
|
|
29
|
+
*/
|
|
30
|
+
export declare function ensureGlobalPromptDir(name: string, configHome?: string): Promise<string[]>;
|
|
22
31
|
export declare function ensurePersonaFiles(personaDir: string): Promise<{
|
|
23
32
|
status: BootstrapStatus;
|
|
24
33
|
filesCreated: string[];
|
|
@@ -93,6 +93,58 @@ async function fileContentDiffersFromTemplate(filePath, template) {
|
|
|
93
93
|
throw err;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
// ─── Generic Prompt Sync ──────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* 确保内置 prompt 目录同步到 ~/.agent/prompts/{name}/。
|
|
99
|
+
*
|
|
100
|
+
* 和 persona 不同(有 bootstrap 状态机),这只是一个简单的文件复制:
|
|
101
|
+
* 首次安装时复制,已存在则跳过。
|
|
102
|
+
*
|
|
103
|
+
* 用于 attention.md 等非 persona 的内置 prompt 文件。
|
|
104
|
+
*/
|
|
105
|
+
export async function ensureGlobalPromptDir(name, configHome = path.join(os.homedir(), '.agent')) {
|
|
106
|
+
const targetDir = path.join(configHome, 'prompts', name);
|
|
107
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
108
|
+
// 搜索源目录(与 loadTemplateContent 的搜索策略一致)
|
|
109
|
+
const sourceDirs = [
|
|
110
|
+
path.join(__dirname, '..', 'prompts', name),
|
|
111
|
+
path.join(process.cwd(), 'src', 'prompts', name),
|
|
112
|
+
path.join(process.cwd(), 'dist', 'prompts', name),
|
|
113
|
+
];
|
|
114
|
+
let sourceDir;
|
|
115
|
+
for (const dir of sourceDirs) {
|
|
116
|
+
try {
|
|
117
|
+
const stat = await fsp.stat(dir);
|
|
118
|
+
if (stat.isDirectory()) {
|
|
119
|
+
sourceDir = dir;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!sourceDir) {
|
|
128
|
+
logger.warn(`Source prompt directory not found: prompts/${name}. Searched: ${sourceDirs.join(', ')}`);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const filesCreated = [];
|
|
132
|
+
const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
135
|
+
continue;
|
|
136
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
137
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
138
|
+
const created = await writeFileIfMissing(targetPath, await fsp.readFile(sourcePath, 'utf-8'));
|
|
139
|
+
if (created) {
|
|
140
|
+
filesCreated.push(`${name}/${entry.name}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (filesCreated.length > 0) {
|
|
144
|
+
logger.info('Synced built-in prompt files', { dir: name, files: filesCreated });
|
|
145
|
+
}
|
|
146
|
+
return filesCreated;
|
|
147
|
+
}
|
|
96
148
|
// ─── Public API ──────────────────────────────────────────────────────
|
|
97
149
|
export async function ensurePersonaFiles(personaDir) {
|
|
98
150
|
await fsp.mkdir(personaDir, { recursive: true });
|
package/null
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyacinth-ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"description": "Hyacinth 🪻 — multi-provider AI agent with tool system and context management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,13 +29,11 @@
|
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
30
|
"chalk": "^5.6.2",
|
|
31
31
|
"chokidar": "^5.0.0",
|
|
32
|
-
"chrome-devtools-mcp": "^1.2.0",
|
|
33
32
|
"commander": "^14.0.3",
|
|
34
33
|
"diff": "^9.0.0",
|
|
35
34
|
"fastify": "^5.8.5",
|
|
36
35
|
"js-tiktoken": "^1.0.21",
|
|
37
36
|
"mammoth": "^1.12.0",
|
|
38
|
-
"mcp-chrome-bridge": "^1.0.31",
|
|
39
37
|
"openai": "^6.39.0",
|
|
40
38
|
"picocolors": "^1.1.1",
|
|
41
39
|
"sharp": "^0.35.0",
|
|
@@ -43,7 +41,9 @@
|
|
|
43
41
|
"xlsx": "^0.18.5"
|
|
44
42
|
},
|
|
45
43
|
"optionalDependencies": {
|
|
46
|
-
"better-sqlite3": "^12.10.0"
|
|
44
|
+
"better-sqlite3": "^12.10.0",
|
|
45
|
+
"chrome-devtools-mcp": "^1.2.0",
|
|
46
|
+
"mcp-chrome-bridge": "^1.0.31"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/better-sqlite3": "^7.6.13",
|