aemeathcli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +607 -0
- package/dist/App-P4MYD4QY.js +2719 -0
- package/dist/App-P4MYD4QY.js.map +1 -0
- package/dist/api-key-fallback-YQQBOQIL.js +11 -0
- package/dist/api-key-fallback-YQQBOQIL.js.map +1 -0
- package/dist/chunk-4IJD72YB.js +184 -0
- package/dist/chunk-4IJD72YB.js.map +1 -0
- package/dist/chunk-6PDJ45T4.js +325 -0
- package/dist/chunk-6PDJ45T4.js.map +1 -0
- package/dist/chunk-CARHU3DO.js +562 -0
- package/dist/chunk-CARHU3DO.js.map +1 -0
- package/dist/chunk-CGEV3ARR.js +80 -0
- package/dist/chunk-CGEV3ARR.js.map +1 -0
- package/dist/chunk-CS5X3BWX.js +27 -0
- package/dist/chunk-CS5X3BWX.js.map +1 -0
- package/dist/chunk-CYQNBB25.js +44 -0
- package/dist/chunk-CYQNBB25.js.map +1 -0
- package/dist/chunk-DAHGLHNR.js +657 -0
- package/dist/chunk-DAHGLHNR.js.map +1 -0
- package/dist/chunk-H66O5Z2V.js +305 -0
- package/dist/chunk-H66O5Z2V.js.map +1 -0
- package/dist/chunk-HCIHOHLX.js +322 -0
- package/dist/chunk-HCIHOHLX.js.map +1 -0
- package/dist/chunk-HMJRPNPZ.js +1031 -0
- package/dist/chunk-HMJRPNPZ.js.map +1 -0
- package/dist/chunk-I5PZ4JTS.js +119 -0
- package/dist/chunk-I5PZ4JTS.js.map +1 -0
- package/dist/chunk-IYW62KKR.js +255 -0
- package/dist/chunk-IYW62KKR.js.map +1 -0
- package/dist/chunk-JAXXTYID.js +51 -0
- package/dist/chunk-JAXXTYID.js.map +1 -0
- package/dist/chunk-LSOYPSAT.js +183 -0
- package/dist/chunk-LSOYPSAT.js.map +1 -0
- package/dist/chunk-MFBHNWGV.js +416 -0
- package/dist/chunk-MFBHNWGV.js.map +1 -0
- package/dist/chunk-MXZSI3AY.js +311 -0
- package/dist/chunk-MXZSI3AY.js.map +1 -0
- package/dist/chunk-NBR3GHMT.js +72 -0
- package/dist/chunk-NBR3GHMT.js.map +1 -0
- package/dist/chunk-O3ZF22SW.js +246 -0
- package/dist/chunk-O3ZF22SW.js.map +1 -0
- package/dist/chunk-SUSJPZU2.js +181 -0
- package/dist/chunk-SUSJPZU2.js.map +1 -0
- package/dist/chunk-TEVZS4FA.js +310 -0
- package/dist/chunk-TEVZS4FA.js.map +1 -0
- package/dist/chunk-UY2SYSEZ.js +211 -0
- package/dist/chunk-UY2SYSEZ.js.map +1 -0
- package/dist/chunk-WAHVZH7V.js +260 -0
- package/dist/chunk-WAHVZH7V.js.map +1 -0
- package/dist/chunk-WPP3PEDE.js +234 -0
- package/dist/chunk-WPP3PEDE.js.map +1 -0
- package/dist/chunk-Y5XVD2CD.js +1610 -0
- package/dist/chunk-Y5XVD2CD.js.map +1 -0
- package/dist/chunk-YL5XFHR3.js +56 -0
- package/dist/chunk-YL5XFHR3.js.map +1 -0
- package/dist/chunk-ZGOHARPV.js +122 -0
- package/dist/chunk-ZGOHARPV.js.map +1 -0
- package/dist/claude-adapter-QMLFMSP3.js +6 -0
- package/dist/claude-adapter-QMLFMSP3.js.map +1 -0
- package/dist/claude-login-5WELXPKT.js +324 -0
- package/dist/claude-login-5WELXPKT.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +703 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-login-7HHLJHBF.js +164 -0
- package/dist/codex-login-7HHLJHBF.js.map +1 -0
- package/dist/config-store-W6FBCQAQ.js +6 -0
- package/dist/config-store-W6FBCQAQ.js.map +1 -0
- package/dist/executor-6RIKIGXK.js +4 -0
- package/dist/executor-6RIKIGXK.js.map +1 -0
- package/dist/gemini-adapter-6JIHZ7WI.js +6 -0
- package/dist/gemini-adapter-6JIHZ7WI.js.map +1 -0
- package/dist/gemini-login-ZZLYC3J6.js +346 -0
- package/dist/gemini-login-ZZLYC3J6.js.map +1 -0
- package/dist/index.d.ts +2210 -0
- package/dist/index.js +1419 -0
- package/dist/index.js.map +1 -0
- package/dist/kimi-adapter-JN4HFFHU.js +6 -0
- package/dist/kimi-adapter-JN4HFFHU.js.map +1 -0
- package/dist/kimi-login-CZPS63NK.js +149 -0
- package/dist/kimi-login-CZPS63NK.js.map +1 -0
- package/dist/native-cli-adapters-OLW3XX57.js +6 -0
- package/dist/native-cli-adapters-OLW3XX57.js.map +1 -0
- package/dist/ollama-adapter-OJQ3FKWK.js +6 -0
- package/dist/ollama-adapter-OJQ3FKWK.js.map +1 -0
- package/dist/openai-adapter-XU46EN7B.js +6 -0
- package/dist/openai-adapter-XU46EN7B.js.map +1 -0
- package/dist/registry-4KD24ZC3.js +6 -0
- package/dist/registry-4KD24ZC3.js.map +1 -0
- package/dist/registry-H7B3AHPQ.js +5 -0
- package/dist/registry-H7B3AHPQ.js.map +1 -0
- package/dist/server-manager-PTGBHCLS.js +5 -0
- package/dist/server-manager-PTGBHCLS.js.map +1 -0
- package/dist/session-manager-ECEEACGY.js +12 -0
- package/dist/session-manager-ECEEACGY.js.map +1 -0
- package/dist/team-manager-HC4XGCFY.js +11 -0
- package/dist/team-manager-HC4XGCFY.js.map +1 -0
- package/dist/tmux-manager-GPYZ3WQH.js +6 -0
- package/dist/tmux-manager-GPYZ3WQH.js.map +1 -0
- package/dist/tools-TSMXMHIF.js +6 -0
- package/dist/tools-TSMXMHIF.js.map +1 -0
- package/package.json +89 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
export { AgentProcess, MessageBus, PlanApproval, TaskStore, TeamManager } from './chunk-HMJRPNPZ.js';
|
|
2
|
+
export { MCPClient, MCPServerManager } from './chunk-MFBHNWGV.js';
|
|
3
|
+
export { SkillLoader, SkillRegistry } from './chunk-TEVZS4FA.js';
|
|
4
|
+
export { ConfigStore } from './chunk-IYW62KKR.js';
|
|
5
|
+
export { SkillExecutor } from './chunk-LSOYPSAT.js';
|
|
6
|
+
export { ContextManager, CostTracker, ModelRouter, PermissionManager, TaskOrchestrator, createModelRouter } from './chunk-DAHGLHNR.js';
|
|
7
|
+
export { ToolRegistry, createDefaultRegistry } from './chunk-Y5XVD2CD.js';
|
|
8
|
+
export { ClaudeAdapter } from './chunk-WAHVZH7V.js';
|
|
9
|
+
export { ClaudeNativeCLIAdapter, CodexNativeCLIAdapter, GeminiNativeCLIAdapter, KimiNativeCLIAdapter } from './chunk-6PDJ45T4.js';
|
|
10
|
+
export { OpenAIAdapter } from './chunk-WPP3PEDE.js';
|
|
11
|
+
export { GeminiAdapter } from './chunk-UY2SYSEZ.js';
|
|
12
|
+
export { KimiAdapter } from './chunk-MXZSI3AY.js';
|
|
13
|
+
export { OllamaAdapter } from './chunk-H66O5Z2V.js';
|
|
14
|
+
export { ProviderRegistry } from './chunk-O3ZF22SW.js';
|
|
15
|
+
export { LayoutEngine, TmuxManager } from './chunk-CARHU3DO.js';
|
|
16
|
+
import { getEventBus } from './chunk-YL5XFHR3.js';
|
|
17
|
+
export { getEventBus } from './chunk-YL5XFHR3.js';
|
|
18
|
+
export { SessionManager } from './chunk-SUSJPZU2.js';
|
|
19
|
+
import './chunk-I5PZ4JTS.js';
|
|
20
|
+
export { CredentialStore } from './chunk-4IJD72YB.js';
|
|
21
|
+
import { withRetry, sleep } from './chunk-CGEV3ARR.js';
|
|
22
|
+
export { DEFAULT_CONFIG } from './chunk-CYQNBB25.js';
|
|
23
|
+
import { getDatabasePath, ensureDirectory, getDatabaseDir, getIPCSocketPath, ensureSecureDirectory, getIPCSocketDir } from './chunk-NBR3GHMT.js';
|
|
24
|
+
import './chunk-CS5X3BWX.js';
|
|
25
|
+
export { DEFAULT_MODEL_ID, SUPPORTED_MODELS } from './chunk-HCIHOHLX.js';
|
|
26
|
+
import { AgentSpawnError, IPCError, ToolCallError } from './chunk-ZGOHARPV.js';
|
|
27
|
+
export { AemeathError, AgentSpawnError, AuthenticationError, ContextOverflowError, ExecutionTimeoutError, FileNotFoundError, IPCError, InvalidConfigError, MissingConfigError, ModelNotFoundError, PermissionDeniedError, RateLimitError, ServerConnectionError, ToolCallError } from './chunk-ZGOHARPV.js';
|
|
28
|
+
import { logger } from './chunk-JAXXTYID.js';
|
|
29
|
+
import Database from 'better-sqlite3';
|
|
30
|
+
import { chmodSync } from 'fs';
|
|
31
|
+
import { randomUUID, randomBytes, createHmac, timingSafeEqual } from 'crypto';
|
|
32
|
+
import { execa } from 'execa';
|
|
33
|
+
import { createServer, connect } from 'net';
|
|
34
|
+
import { chmod, unlink } from 'fs/promises';
|
|
35
|
+
|
|
36
|
+
// src/storage/migrations/001-initial.ts
|
|
37
|
+
var CREATE_CONVERSATIONS = `
|
|
38
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
project_root TEXT NOT NULL,
|
|
41
|
+
default_model TEXT,
|
|
42
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
43
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
44
|
+
metadata TEXT DEFAULT '{}'
|
|
45
|
+
)`;
|
|
46
|
+
var CREATE_MESSAGES = `
|
|
47
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
50
|
+
role TEXT NOT NULL,
|
|
51
|
+
model TEXT,
|
|
52
|
+
provider TEXT,
|
|
53
|
+
content TEXT NOT NULL,
|
|
54
|
+
tool_calls TEXT,
|
|
55
|
+
token_usage TEXT,
|
|
56
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
57
|
+
)`;
|
|
58
|
+
var CREATE_FILE_CONTEXT = `
|
|
59
|
+
CREATE TABLE IF NOT EXISTS file_context (
|
|
60
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
62
|
+
file_path TEXT NOT NULL,
|
|
63
|
+
content_hash TEXT,
|
|
64
|
+
token_count INTEGER,
|
|
65
|
+
added_at TEXT DEFAULT (datetime('now'))
|
|
66
|
+
)`;
|
|
67
|
+
var CREATE_COST_TRACKING = `
|
|
68
|
+
CREATE TABLE IF NOT EXISTS cost_tracking (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
71
|
+
provider TEXT NOT NULL,
|
|
72
|
+
model TEXT NOT NULL,
|
|
73
|
+
role TEXT,
|
|
74
|
+
input_tokens INTEGER,
|
|
75
|
+
output_tokens INTEGER,
|
|
76
|
+
cost_usd REAL,
|
|
77
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
78
|
+
)`;
|
|
79
|
+
var CREATE_TEAMS = `
|
|
80
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
81
|
+
id TEXT PRIMARY KEY,
|
|
82
|
+
name TEXT NOT NULL,
|
|
83
|
+
status TEXT DEFAULT 'active',
|
|
84
|
+
config TEXT,
|
|
85
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
86
|
+
)`;
|
|
87
|
+
var CREATE_INDEXES = [
|
|
88
|
+
"CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)",
|
|
89
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_conversation ON cost_tracking(conversation_id)",
|
|
90
|
+
"CREATE INDEX IF NOT EXISTS idx_cost_provider ON cost_tracking(provider)",
|
|
91
|
+
"CREATE INDEX IF NOT EXISTS idx_file_context_conversation ON file_context(conversation_id)"
|
|
92
|
+
];
|
|
93
|
+
function up(db) {
|
|
94
|
+
db.transaction(() => {
|
|
95
|
+
db.exec(CREATE_CONVERSATIONS);
|
|
96
|
+
db.exec(CREATE_MESSAGES);
|
|
97
|
+
db.exec(CREATE_FILE_CONTEXT);
|
|
98
|
+
db.exec(CREATE_COST_TRACKING);
|
|
99
|
+
db.exec(CREATE_TEAMS);
|
|
100
|
+
for (const sql of CREATE_INDEXES) {
|
|
101
|
+
db.exec(sql);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/storage/sqlite-store.ts
|
|
107
|
+
var MIGRATIONS_TABLE_DDL = `
|
|
108
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
109
|
+
id TEXT PRIMARY KEY,
|
|
110
|
+
applied_at TEXT DEFAULT (datetime('now'))
|
|
111
|
+
)`;
|
|
112
|
+
var MIGRATIONS = [
|
|
113
|
+
{ id: "001-initial", up }
|
|
114
|
+
];
|
|
115
|
+
var SqliteStore = class {
|
|
116
|
+
db;
|
|
117
|
+
closed = false;
|
|
118
|
+
get database() {
|
|
119
|
+
if (this.closed || !this.db) {
|
|
120
|
+
throw new Error("SqliteStore is closed or not initialized");
|
|
121
|
+
}
|
|
122
|
+
return this.db;
|
|
123
|
+
}
|
|
124
|
+
open(dbPath) {
|
|
125
|
+
if (this.db) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const resolvedPath = dbPath ?? getDatabasePath();
|
|
129
|
+
ensureDirectory(getDatabaseDir());
|
|
130
|
+
logger.info({ path: resolvedPath }, "Opening SQLite database");
|
|
131
|
+
this.db = new Database(resolvedPath);
|
|
132
|
+
this.db.pragma("journal_mode = WAL");
|
|
133
|
+
this.db.pragma("busy_timeout = 5000");
|
|
134
|
+
this.db.pragma("foreign_keys = ON");
|
|
135
|
+
this.db.pragma("synchronous = NORMAL");
|
|
136
|
+
try {
|
|
137
|
+
chmodSync(resolvedPath, 384);
|
|
138
|
+
} catch {
|
|
139
|
+
logger.warn(
|
|
140
|
+
{ path: resolvedPath },
|
|
141
|
+
"Could not set database file permissions to 600"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
this.runMigrations();
|
|
145
|
+
this.registerCleanupHandlers();
|
|
146
|
+
}
|
|
147
|
+
runMigrations() {
|
|
148
|
+
const db = this.database;
|
|
149
|
+
db.exec(MIGRATIONS_TABLE_DDL);
|
|
150
|
+
const appliedStmt = db.prepare(
|
|
151
|
+
"SELECT id FROM _migrations WHERE id = ?"
|
|
152
|
+
);
|
|
153
|
+
const insertStmt = db.prepare(
|
|
154
|
+
"INSERT INTO _migrations (id) VALUES (?)"
|
|
155
|
+
);
|
|
156
|
+
for (const migration of MIGRATIONS) {
|
|
157
|
+
const existing = appliedStmt.get(migration.id);
|
|
158
|
+
if (!existing) {
|
|
159
|
+
logger.info({ migrationId: migration.id }, "Running migration");
|
|
160
|
+
db.transaction(() => {
|
|
161
|
+
migration.up(db);
|
|
162
|
+
insertStmt.run(migration.id);
|
|
163
|
+
})();
|
|
164
|
+
logger.info({ migrationId: migration.id }, "Migration applied");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
prepare(sql) {
|
|
169
|
+
return this.database.prepare(sql);
|
|
170
|
+
}
|
|
171
|
+
run(sql, ...params) {
|
|
172
|
+
return this.database.prepare(sql).run(...params);
|
|
173
|
+
}
|
|
174
|
+
get(sql, ...params) {
|
|
175
|
+
return this.database.prepare(sql).get(...params);
|
|
176
|
+
}
|
|
177
|
+
all(sql, ...params) {
|
|
178
|
+
return this.database.prepare(sql).all(...params);
|
|
179
|
+
}
|
|
180
|
+
transaction(fn) {
|
|
181
|
+
return this.database.transaction(fn)();
|
|
182
|
+
}
|
|
183
|
+
close() {
|
|
184
|
+
if (this.closed || !this.db) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
logger.info("Closing SQLite database");
|
|
188
|
+
this.closed = true;
|
|
189
|
+
try {
|
|
190
|
+
this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
191
|
+
this.db.close();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
logger.error({ error: message }, "Error closing database");
|
|
195
|
+
}
|
|
196
|
+
this.db = void 0;
|
|
197
|
+
}
|
|
198
|
+
registerCleanupHandlers() {
|
|
199
|
+
const cleanup = () => {
|
|
200
|
+
this.close();
|
|
201
|
+
};
|
|
202
|
+
process.on("exit", cleanup);
|
|
203
|
+
process.on("SIGINT", () => {
|
|
204
|
+
cleanup();
|
|
205
|
+
process.exit(130);
|
|
206
|
+
});
|
|
207
|
+
process.on("SIGTERM", () => {
|
|
208
|
+
cleanup();
|
|
209
|
+
process.exit(143);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var ConversationStore = class {
|
|
214
|
+
store;
|
|
215
|
+
constructor(store) {
|
|
216
|
+
this.store = store;
|
|
217
|
+
}
|
|
218
|
+
createConversation(projectRoot, defaultModel, metadata) {
|
|
219
|
+
const id = randomUUID();
|
|
220
|
+
const metadataJson = JSON.stringify(metadata ?? {});
|
|
221
|
+
this.store.run(
|
|
222
|
+
`INSERT INTO conversations (id, project_root, default_model, metadata)
|
|
223
|
+
VALUES (?, ?, ?, ?)`,
|
|
224
|
+
id,
|
|
225
|
+
projectRoot,
|
|
226
|
+
defaultModel ?? null,
|
|
227
|
+
metadataJson
|
|
228
|
+
);
|
|
229
|
+
logger.info({ conversationId: id, projectRoot }, "Conversation created");
|
|
230
|
+
const row = this.store.get(
|
|
231
|
+
"SELECT * FROM conversations WHERE id = ?",
|
|
232
|
+
id
|
|
233
|
+
);
|
|
234
|
+
if (!row) {
|
|
235
|
+
throw new Error(`Failed to retrieve created conversation: ${id}`);
|
|
236
|
+
}
|
|
237
|
+
return this.mapConversationRow(row);
|
|
238
|
+
}
|
|
239
|
+
getConversation(id) {
|
|
240
|
+
const row = this.store.get(
|
|
241
|
+
"SELECT * FROM conversations WHERE id = ?",
|
|
242
|
+
id
|
|
243
|
+
);
|
|
244
|
+
return row ? this.mapConversationRow(row) : void 0;
|
|
245
|
+
}
|
|
246
|
+
listConversations(projectRoot) {
|
|
247
|
+
const rows = projectRoot ? this.store.all(
|
|
248
|
+
"SELECT * FROM conversations WHERE project_root = ? ORDER BY updated_at DESC",
|
|
249
|
+
projectRoot
|
|
250
|
+
) : this.store.all(
|
|
251
|
+
"SELECT * FROM conversations ORDER BY updated_at DESC"
|
|
252
|
+
);
|
|
253
|
+
return rows.map((row) => this.mapConversationRow(row));
|
|
254
|
+
}
|
|
255
|
+
deleteConversation(id) {
|
|
256
|
+
this.store.run("DELETE FROM conversations WHERE id = ?", id);
|
|
257
|
+
logger.info({ conversationId: id }, "Conversation deleted");
|
|
258
|
+
}
|
|
259
|
+
addMessage(params) {
|
|
260
|
+
const toolCallsJson = params.toolCalls ? JSON.stringify(params.toolCalls) : null;
|
|
261
|
+
const tokenUsageJson = params.tokenUsage ? JSON.stringify(params.tokenUsage) : null;
|
|
262
|
+
const result = this.store.run(
|
|
263
|
+
`INSERT INTO messages
|
|
264
|
+
(conversation_id, role, model, provider, content, tool_calls, token_usage)
|
|
265
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
266
|
+
params.conversationId,
|
|
267
|
+
params.role,
|
|
268
|
+
params.model ?? null,
|
|
269
|
+
params.provider ?? null,
|
|
270
|
+
params.content,
|
|
271
|
+
toolCallsJson,
|
|
272
|
+
tokenUsageJson
|
|
273
|
+
);
|
|
274
|
+
this.store.run(
|
|
275
|
+
"UPDATE conversations SET updated_at = datetime('now') WHERE id = ?",
|
|
276
|
+
params.conversationId
|
|
277
|
+
);
|
|
278
|
+
const row = this.store.get(
|
|
279
|
+
"SELECT * FROM messages WHERE id = ?",
|
|
280
|
+
result.lastInsertRowid
|
|
281
|
+
);
|
|
282
|
+
if (!row) {
|
|
283
|
+
throw new Error("Failed to retrieve created message");
|
|
284
|
+
}
|
|
285
|
+
return this.mapMessageRow(row);
|
|
286
|
+
}
|
|
287
|
+
getMessages(conversationId, pagination) {
|
|
288
|
+
const limit = pagination?.limit ?? 100;
|
|
289
|
+
const offset = pagination?.offset ?? 0;
|
|
290
|
+
const rows = this.store.all(
|
|
291
|
+
`SELECT * FROM messages
|
|
292
|
+
WHERE conversation_id = ?
|
|
293
|
+
ORDER BY created_at ASC
|
|
294
|
+
LIMIT ? OFFSET ?`,
|
|
295
|
+
conversationId,
|
|
296
|
+
limit,
|
|
297
|
+
offset
|
|
298
|
+
);
|
|
299
|
+
return rows.map((row) => this.mapMessageRow(row));
|
|
300
|
+
}
|
|
301
|
+
getMessageCount(conversationId) {
|
|
302
|
+
const result = this.store.get(
|
|
303
|
+
"SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?",
|
|
304
|
+
conversationId
|
|
305
|
+
);
|
|
306
|
+
return result?.count ?? 0;
|
|
307
|
+
}
|
|
308
|
+
addFileContext(params) {
|
|
309
|
+
const result = this.store.run(
|
|
310
|
+
`INSERT INTO file_context
|
|
311
|
+
(conversation_id, file_path, content_hash, token_count)
|
|
312
|
+
VALUES (?, ?, ?, ?)`,
|
|
313
|
+
params.conversationId,
|
|
314
|
+
params.filePath,
|
|
315
|
+
params.contentHash ?? null,
|
|
316
|
+
params.tokenCount ?? null
|
|
317
|
+
);
|
|
318
|
+
const row = this.store.get(
|
|
319
|
+
"SELECT * FROM file_context WHERE id = ?",
|
|
320
|
+
result.lastInsertRowid
|
|
321
|
+
);
|
|
322
|
+
if (!row) {
|
|
323
|
+
throw new Error("Failed to retrieve created file context");
|
|
324
|
+
}
|
|
325
|
+
return this.mapFileContextRow(row);
|
|
326
|
+
}
|
|
327
|
+
getFileContext(conversationId) {
|
|
328
|
+
const rows = this.store.all(
|
|
329
|
+
"SELECT * FROM file_context WHERE conversation_id = ? ORDER BY added_at DESC",
|
|
330
|
+
conversationId
|
|
331
|
+
);
|
|
332
|
+
return rows.map((row) => this.mapFileContextRow(row));
|
|
333
|
+
}
|
|
334
|
+
removeFileContext(conversationId, filePath) {
|
|
335
|
+
this.store.run(
|
|
336
|
+
"DELETE FROM file_context WHERE conversation_id = ? AND file_path = ?",
|
|
337
|
+
conversationId,
|
|
338
|
+
filePath
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
addCost(params) {
|
|
342
|
+
const result = this.store.run(
|
|
343
|
+
`INSERT INTO cost_tracking
|
|
344
|
+
(conversation_id, provider, model, role, input_tokens, output_tokens, cost_usd)
|
|
345
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
346
|
+
params.conversationId,
|
|
347
|
+
params.provider,
|
|
348
|
+
params.model,
|
|
349
|
+
params.role ?? null,
|
|
350
|
+
params.inputTokens ?? null,
|
|
351
|
+
params.outputTokens ?? null,
|
|
352
|
+
params.costUsd ?? null
|
|
353
|
+
);
|
|
354
|
+
const row = this.store.get(
|
|
355
|
+
"SELECT * FROM cost_tracking WHERE id = ?",
|
|
356
|
+
result.lastInsertRowid
|
|
357
|
+
);
|
|
358
|
+
if (!row) {
|
|
359
|
+
throw new Error("Failed to retrieve created cost entry");
|
|
360
|
+
}
|
|
361
|
+
return this.mapCostRow(row);
|
|
362
|
+
}
|
|
363
|
+
getConversationCost(conversationId) {
|
|
364
|
+
const result = this.store.get(
|
|
365
|
+
"SELECT SUM(cost_usd) as total FROM cost_tracking WHERE conversation_id = ?",
|
|
366
|
+
conversationId
|
|
367
|
+
);
|
|
368
|
+
return result?.total ?? 0;
|
|
369
|
+
}
|
|
370
|
+
getCostBreakdown(conversationId) {
|
|
371
|
+
const rows = this.store.all(
|
|
372
|
+
"SELECT * FROM cost_tracking WHERE conversation_id = ? ORDER BY created_at ASC",
|
|
373
|
+
conversationId
|
|
374
|
+
);
|
|
375
|
+
return rows.map((row) => this.mapCostRow(row));
|
|
376
|
+
}
|
|
377
|
+
// ── Private row mappers ─────────────────────────────────────────────
|
|
378
|
+
mapConversationRow(row) {
|
|
379
|
+
let metadata = {};
|
|
380
|
+
try {
|
|
381
|
+
metadata = JSON.parse(row.metadata);
|
|
382
|
+
} catch {
|
|
383
|
+
metadata = {};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
id: row.id,
|
|
387
|
+
projectRoot: row.project_root,
|
|
388
|
+
defaultModel: row.default_model,
|
|
389
|
+
createdAt: row.created_at,
|
|
390
|
+
updatedAt: row.updated_at,
|
|
391
|
+
metadata
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
mapMessageRow(row) {
|
|
395
|
+
let toolCalls = null;
|
|
396
|
+
if (row.tool_calls) {
|
|
397
|
+
try {
|
|
398
|
+
toolCalls = JSON.parse(row.tool_calls);
|
|
399
|
+
} catch {
|
|
400
|
+
toolCalls = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
let tokenUsage = null;
|
|
404
|
+
if (row.token_usage) {
|
|
405
|
+
try {
|
|
406
|
+
tokenUsage = JSON.parse(row.token_usage);
|
|
407
|
+
} catch {
|
|
408
|
+
tokenUsage = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
id: row.id,
|
|
413
|
+
conversationId: row.conversation_id,
|
|
414
|
+
role: row.role,
|
|
415
|
+
model: row.model,
|
|
416
|
+
provider: row.provider,
|
|
417
|
+
content: row.content,
|
|
418
|
+
toolCalls,
|
|
419
|
+
tokenUsage,
|
|
420
|
+
createdAt: row.created_at
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
mapFileContextRow(row) {
|
|
424
|
+
return {
|
|
425
|
+
id: row.id,
|
|
426
|
+
conversationId: row.conversation_id,
|
|
427
|
+
filePath: row.file_path,
|
|
428
|
+
contentHash: row.content_hash,
|
|
429
|
+
tokenCount: row.token_count,
|
|
430
|
+
addedAt: row.added_at
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
mapCostRow(row) {
|
|
434
|
+
return {
|
|
435
|
+
id: row.id,
|
|
436
|
+
conversationId: row.conversation_id,
|
|
437
|
+
provider: row.provider,
|
|
438
|
+
model: row.model,
|
|
439
|
+
role: row.role,
|
|
440
|
+
inputTokens: row.input_tokens,
|
|
441
|
+
outputTokens: row.output_tokens,
|
|
442
|
+
costUsd: row.cost_usd,
|
|
443
|
+
createdAt: row.created_at
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
var ROLE_PROFILES = {
|
|
448
|
+
planning: { backgroundColor: "#1a1a2e", badgeText: "Planner" },
|
|
449
|
+
coding: { backgroundColor: "#162447", badgeText: "Coder" },
|
|
450
|
+
review: { backgroundColor: "#1b2a1b", badgeText: "Reviewer" },
|
|
451
|
+
testing: { backgroundColor: "#2a1b1b", badgeText: "Tester" },
|
|
452
|
+
bugfix: { backgroundColor: "#2a2a1b", badgeText: "Debugger" },
|
|
453
|
+
documentation: { backgroundColor: "#1b1b2a", badgeText: "Docs" }
|
|
454
|
+
};
|
|
455
|
+
var DEFAULT_PROFILE = {
|
|
456
|
+
backgroundColor: "#1e1e1e",
|
|
457
|
+
badgeText: "Agent"
|
|
458
|
+
};
|
|
459
|
+
var ITerm2Manager = class {
|
|
460
|
+
panes = /* @__PURE__ */ new Map();
|
|
461
|
+
disposed = false;
|
|
462
|
+
/**
|
|
463
|
+
* Check if currently running inside iTerm2 on macOS.
|
|
464
|
+
*/
|
|
465
|
+
isAvailable() {
|
|
466
|
+
if (process.platform !== "darwin") {
|
|
467
|
+
logger.debug("iTerm2 manager is macOS-only");
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const termProgram = process.env["TERM_PROGRAM"];
|
|
471
|
+
if (termProgram !== "iTerm.app") {
|
|
472
|
+
logger.debug({ termProgram }, "Not running inside iTerm2");
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Check if the iTerm2 Python API is accessible.
|
|
479
|
+
*/
|
|
480
|
+
async isPythonAPIAvailable() {
|
|
481
|
+
if (!this.isAvailable()) return false;
|
|
482
|
+
try {
|
|
483
|
+
const result = await execa("python3", ["-c", "import iterm2"]);
|
|
484
|
+
return result.exitCode === 0;
|
|
485
|
+
} catch {
|
|
486
|
+
logger.warn("iTerm2 Python API module (iterm2) not installed");
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create panes in the current iTerm2 window based on layout config.
|
|
492
|
+
*/
|
|
493
|
+
async createPanes(layoutConfig) {
|
|
494
|
+
this.assertNotDisposed();
|
|
495
|
+
if (!await this.isPythonAPIAvailable()) {
|
|
496
|
+
throw new AgentSpawnError(
|
|
497
|
+
"iterm2",
|
|
498
|
+
"iTerm2 Python API is not available. Install with: pip3 install iterm2"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const script = this.buildCreatePanesScript(layoutConfig);
|
|
502
|
+
try {
|
|
503
|
+
await this.executePythonScript(script);
|
|
504
|
+
logger.info(
|
|
505
|
+
{ paneCount: layoutConfig.panes.length },
|
|
506
|
+
"iTerm2 panes created"
|
|
507
|
+
);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
510
|
+
throw new AgentSpawnError("iterm2", `Failed to create iTerm2 panes: ${message}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Send a command to a specific pane via iTerm2 Python API.
|
|
515
|
+
*/
|
|
516
|
+
async sendCommand(paneId, command) {
|
|
517
|
+
this.assertNotDisposed();
|
|
518
|
+
const info = this.panes.get(paneId);
|
|
519
|
+
if (!info) {
|
|
520
|
+
logger.warn({ paneId }, "iTerm2 pane not found, cannot send command");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const escapedCommand = command.replace(/"/g, '\\"').replace(/\\/g, "\\\\");
|
|
524
|
+
const script = this.buildSendCommandScript(info.sessionId, escapedCommand);
|
|
525
|
+
try {
|
|
526
|
+
await this.executePythonScript(script);
|
|
527
|
+
logger.debug({ paneId, command: command.slice(0, 80) }, "Command sent to iTerm2 pane");
|
|
528
|
+
} catch (error) {
|
|
529
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
530
|
+
logger.error({ paneId, error: message }, "Failed to send command to iTerm2 pane");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Set the profile (colors) for a pane based on agent role.
|
|
535
|
+
*/
|
|
536
|
+
async setProfile(paneId, role) {
|
|
537
|
+
this.assertNotDisposed();
|
|
538
|
+
const info = this.panes.get(paneId);
|
|
539
|
+
if (!info) return;
|
|
540
|
+
const profile = ROLE_PROFILES[role] ?? DEFAULT_PROFILE;
|
|
541
|
+
const script = this.buildSetProfileScript(info.sessionId, profile);
|
|
542
|
+
try {
|
|
543
|
+
await this.executePythonScript(script);
|
|
544
|
+
logger.debug({ paneId, role }, "iTerm2 pane profile set");
|
|
545
|
+
} catch {
|
|
546
|
+
logger.debug({ paneId, role }, "Failed to set iTerm2 profile (non-fatal)");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Destroy all managed iTerm2 panes.
|
|
551
|
+
*/
|
|
552
|
+
async destroy() {
|
|
553
|
+
if (this.disposed) return;
|
|
554
|
+
this.disposed = true;
|
|
555
|
+
for (const [paneId, info] of this.panes) {
|
|
556
|
+
try {
|
|
557
|
+
const script = this.buildCloseSessionScript(info.sessionId);
|
|
558
|
+
await this.executePythonScript(script);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
getEventBus().emit("pane:closed", { paneId });
|
|
562
|
+
}
|
|
563
|
+
this.panes.clear();
|
|
564
|
+
logger.info("iTerm2 panes destroyed");
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Get all tracked pane info.
|
|
568
|
+
*/
|
|
569
|
+
getPanes() {
|
|
570
|
+
return this.panes;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Register a pane that was created externally.
|
|
574
|
+
*/
|
|
575
|
+
registerPane(info) {
|
|
576
|
+
this.panes.set(info.paneId, info);
|
|
577
|
+
getEventBus().emit("pane:created", {
|
|
578
|
+
paneId: info.paneId,
|
|
579
|
+
agentName: info.agentName
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// ── Python Script Builders ─────────────────────────────────────────
|
|
583
|
+
buildCreatePanesScript(layoutConfig) {
|
|
584
|
+
const paneConfigs = layoutConfig.panes;
|
|
585
|
+
const splitCommands = this.buildSplitCommands(paneConfigs, layoutConfig.layout);
|
|
586
|
+
return `
|
|
587
|
+
import iterm2
|
|
588
|
+
import asyncio
|
|
589
|
+
|
|
590
|
+
async def main(connection):
|
|
591
|
+
app = await iterm2.async_get_app(connection)
|
|
592
|
+
window = app.current_terminal_window
|
|
593
|
+
if window is None:
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
tab = window.current_tab
|
|
597
|
+
root_session = tab.current_session
|
|
598
|
+
sessions = [root_session]
|
|
599
|
+
|
|
600
|
+
${splitCommands}
|
|
601
|
+
|
|
602
|
+
# Output session IDs for tracking
|
|
603
|
+
for s in sessions:
|
|
604
|
+
print(s.session_id)
|
|
605
|
+
|
|
606
|
+
iterm2.run_until_complete(main)
|
|
607
|
+
`.trim();
|
|
608
|
+
}
|
|
609
|
+
buildSplitCommands(panes, layout) {
|
|
610
|
+
const lines = [];
|
|
611
|
+
for (let i = 1; i < panes.length; i++) {
|
|
612
|
+
const vertical = this.shouldSplitVertically(i, panes.length, layout);
|
|
613
|
+
const direction = vertical ? "True" : "False";
|
|
614
|
+
lines.push(
|
|
615
|
+
` new_session = await sessions[${Math.floor(i / 2)}].async_split_pane(vertical=${direction})`
|
|
616
|
+
);
|
|
617
|
+
lines.push(` sessions.append(new_session)`);
|
|
618
|
+
const pane = panes[i];
|
|
619
|
+
if (pane) {
|
|
620
|
+
const escapedTitle = pane.title.replace(/'/g, "\\'");
|
|
621
|
+
lines.push(` await new_session.async_set_name("${escapedTitle}")`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return lines.join("\n");
|
|
625
|
+
}
|
|
626
|
+
shouldSplitVertically(index, total, layout) {
|
|
627
|
+
if (layout === "vertical") return true;
|
|
628
|
+
if (layout === "horizontal") return false;
|
|
629
|
+
if (total <= 2) return false;
|
|
630
|
+
return index % 2 === 1;
|
|
631
|
+
}
|
|
632
|
+
buildSendCommandScript(sessionId, command) {
|
|
633
|
+
return `
|
|
634
|
+
import iterm2
|
|
635
|
+
|
|
636
|
+
async def main(connection):
|
|
637
|
+
app = await iterm2.async_get_app(connection)
|
|
638
|
+
session = app.get_session_by_id("${sessionId}")
|
|
639
|
+
if session:
|
|
640
|
+
await session.async_send_text("${command}\\n")
|
|
641
|
+
|
|
642
|
+
iterm2.run_until_complete(main)
|
|
643
|
+
`.trim();
|
|
644
|
+
}
|
|
645
|
+
buildSetProfileScript(sessionId, profile) {
|
|
646
|
+
return `
|
|
647
|
+
import iterm2
|
|
648
|
+
|
|
649
|
+
async def main(connection):
|
|
650
|
+
app = await iterm2.async_get_app(connection)
|
|
651
|
+
session = app.get_session_by_id("${sessionId}")
|
|
652
|
+
if session:
|
|
653
|
+
change = iterm2.LocalWriteOnlyProfile()
|
|
654
|
+
color = iterm2.Color.from_hex("${profile.backgroundColor}")
|
|
655
|
+
change.set_background_color(color)
|
|
656
|
+
change.set_badge_text("${profile.badgeText}")
|
|
657
|
+
await session.async_set_profile_properties(change)
|
|
658
|
+
|
|
659
|
+
iterm2.run_until_complete(main)
|
|
660
|
+
`.trim();
|
|
661
|
+
}
|
|
662
|
+
buildCloseSessionScript(sessionId) {
|
|
663
|
+
return `
|
|
664
|
+
import iterm2
|
|
665
|
+
|
|
666
|
+
async def main(connection):
|
|
667
|
+
app = await iterm2.async_get_app(connection)
|
|
668
|
+
session = app.get_session_by_id("${sessionId}")
|
|
669
|
+
if session:
|
|
670
|
+
await session.async_close()
|
|
671
|
+
|
|
672
|
+
iterm2.run_until_complete(main)
|
|
673
|
+
`.trim();
|
|
674
|
+
}
|
|
675
|
+
// ── Execution ──────────────────────────────────────────────────────
|
|
676
|
+
async executePythonScript(script) {
|
|
677
|
+
const result = await execa("python3", ["-c", script], {
|
|
678
|
+
timeout: 1e4
|
|
679
|
+
});
|
|
680
|
+
return result.stdout;
|
|
681
|
+
}
|
|
682
|
+
assertNotDisposed() {
|
|
683
|
+
if (this.disposed) {
|
|
684
|
+
throw new AgentSpawnError("iterm2", "ITerm2Manager has been disposed");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
var NEWLINE = 10;
|
|
689
|
+
var SOCKET_PERMS = 448;
|
|
690
|
+
var MAX_MSG_SIZE = 1048576;
|
|
691
|
+
var RPC_PARSE_ERROR = -32700;
|
|
692
|
+
var RPC_INVALID_REQ = -32600;
|
|
693
|
+
var RPC_METHOD_NOT_FOUND = -32601;
|
|
694
|
+
var RPC_INTERNAL_ERROR = -32603;
|
|
695
|
+
var RPC_AUTH_ERROR = -32e3;
|
|
696
|
+
var IPCHub = class {
|
|
697
|
+
sessionId;
|
|
698
|
+
hmacSecret;
|
|
699
|
+
socketPath;
|
|
700
|
+
clients = /* @__PURE__ */ new Map();
|
|
701
|
+
handlers = /* @__PURE__ */ new Map();
|
|
702
|
+
server;
|
|
703
|
+
disposed = false;
|
|
704
|
+
constructor(sessionId) {
|
|
705
|
+
this.sessionId = sessionId;
|
|
706
|
+
this.hmacSecret = randomBytes(32).toString("hex");
|
|
707
|
+
this.socketPath = getIPCSocketPath(sessionId);
|
|
708
|
+
this.registerDefaultHandlers();
|
|
709
|
+
}
|
|
710
|
+
getHmacSecret() {
|
|
711
|
+
return this.hmacSecret;
|
|
712
|
+
}
|
|
713
|
+
getSocketPath() {
|
|
714
|
+
return this.socketPath;
|
|
715
|
+
}
|
|
716
|
+
getClientCount() {
|
|
717
|
+
return this.clients.size;
|
|
718
|
+
}
|
|
719
|
+
getConnectedAgentIds() {
|
|
720
|
+
return [...this.clients.keys()];
|
|
721
|
+
}
|
|
722
|
+
/** Start the Unix domain socket server. */
|
|
723
|
+
async start() {
|
|
724
|
+
this.assertAlive();
|
|
725
|
+
ensureSecureDirectory(getIPCSocketDir());
|
|
726
|
+
await this.removeStaleSocket();
|
|
727
|
+
return new Promise((resolve, reject) => {
|
|
728
|
+
this.server = createServer((socket) => this.handleConnection(socket));
|
|
729
|
+
this.server.on("error", (err) => {
|
|
730
|
+
logger.error({ error: err.message }, "IPC server error");
|
|
731
|
+
reject(new IPCError(`Server error: ${err.message}`));
|
|
732
|
+
});
|
|
733
|
+
this.server.listen(this.socketPath, async () => {
|
|
734
|
+
try {
|
|
735
|
+
await chmod(this.socketPath, SOCKET_PERMS);
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
logger.info({ socketPath: this.socketPath }, "IPC hub listening");
|
|
739
|
+
this.setupProcessCleanup();
|
|
740
|
+
resolve();
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/** Send a message to a specific registered client. */
|
|
745
|
+
sendToClient(agentId, message) {
|
|
746
|
+
this.assertAlive();
|
|
747
|
+
const client = this.clients.get(agentId);
|
|
748
|
+
if (client) this.write(client.socket, message);
|
|
749
|
+
}
|
|
750
|
+
/** Broadcast a message to all connected clients. */
|
|
751
|
+
broadcast(message, excludeId) {
|
|
752
|
+
this.assertAlive();
|
|
753
|
+
for (const [id, client] of this.clients) {
|
|
754
|
+
if (id !== excludeId) this.write(client.socket, message);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** Register a custom method handler. */
|
|
758
|
+
onMethod(method, handler) {
|
|
759
|
+
this.handlers.set(method, handler);
|
|
760
|
+
}
|
|
761
|
+
/** Compute HMAC-SHA256 for a message. */
|
|
762
|
+
computeHmac(message) {
|
|
763
|
+
return createHmac("sha256", this.hmacSecret).update(JSON.stringify(message)).digest("hex");
|
|
764
|
+
}
|
|
765
|
+
/** Register a connected socket as a client (also called by agent.register). */
|
|
766
|
+
registerClientSocket(agentId, agentName, socket) {
|
|
767
|
+
this.clients.set(agentId, { agentId, agentName, socket });
|
|
768
|
+
}
|
|
769
|
+
/** Gracefully shut down the hub and all connections. */
|
|
770
|
+
async destroy() {
|
|
771
|
+
if (this.disposed) return;
|
|
772
|
+
this.disposed = true;
|
|
773
|
+
const msg = { jsonrpc: "2.0", method: "hub.shutdown", params: { reason: "Hub shutting down" } };
|
|
774
|
+
for (const c of this.clients.values()) {
|
|
775
|
+
try {
|
|
776
|
+
this.write(c.socket, msg);
|
|
777
|
+
c.socket.end();
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
this.clients.clear();
|
|
782
|
+
if (this.server) {
|
|
783
|
+
await new Promise((r) => {
|
|
784
|
+
this.server.close(() => r());
|
|
785
|
+
});
|
|
786
|
+
this.server = void 0;
|
|
787
|
+
}
|
|
788
|
+
await this.removeStaleSocket();
|
|
789
|
+
logger.info({ sessionId: this.sessionId }, "IPC hub destroyed");
|
|
790
|
+
}
|
|
791
|
+
// ── Connection Handling ───────────────────────────────────────────
|
|
792
|
+
handleConnection(socket) {
|
|
793
|
+
let buffer = Buffer.alloc(0);
|
|
794
|
+
let agentId;
|
|
795
|
+
socket.on("data", (data) => {
|
|
796
|
+
buffer = Buffer.concat([buffer, data]);
|
|
797
|
+
if (buffer.length > MAX_MSG_SIZE) {
|
|
798
|
+
socket.destroy();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
let idx;
|
|
802
|
+
while ((idx = buffer.indexOf(NEWLINE)) !== -1) {
|
|
803
|
+
const raw = buffer.subarray(0, idx).toString("utf-8");
|
|
804
|
+
buffer = buffer.subarray(idx + 1);
|
|
805
|
+
if (raw.length === 0) continue;
|
|
806
|
+
this.processRaw(raw, socket, agentId).then((id) => {
|
|
807
|
+
if (id) agentId = id;
|
|
808
|
+
}).catch((e) => {
|
|
809
|
+
logger.error({ error: e instanceof Error ? e.message : String(e) }, "IPC msg error");
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
socket.on("close", () => {
|
|
814
|
+
if (agentId) {
|
|
815
|
+
this.clients.delete(agentId);
|
|
816
|
+
logger.info({ agentId }, "Client disconnected");
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
socket.on("error", (err) => {
|
|
820
|
+
logger.error({ error: err.message, agentId }, "Socket error");
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async processRaw(raw, socket, currentId) {
|
|
824
|
+
let parsed;
|
|
825
|
+
try {
|
|
826
|
+
parsed = JSON.parse(raw);
|
|
827
|
+
} catch {
|
|
828
|
+
this.sendError(socket, void 0, RPC_PARSE_ERROR, "Parse error");
|
|
829
|
+
return void 0;
|
|
830
|
+
}
|
|
831
|
+
if (!this.isAuthEnvelope(parsed)) {
|
|
832
|
+
this.sendError(socket, void 0, RPC_INVALID_REQ, "Invalid request format");
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
835
|
+
const { message, hmac } = parsed;
|
|
836
|
+
if (!this.verifyHmac(message, hmac)) {
|
|
837
|
+
this.sendError(socket, message.id, RPC_AUTH_ERROR, "Authentication failed");
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
if (message.jsonrpc !== "2.0" || !message.method) {
|
|
841
|
+
this.sendError(socket, message.id, RPC_INVALID_REQ, "Invalid JSON-RPC 2.0");
|
|
842
|
+
return void 0;
|
|
843
|
+
}
|
|
844
|
+
const handler = this.handlers.get(message.method);
|
|
845
|
+
if (!handler) {
|
|
846
|
+
this.sendError(socket, message.id, RPC_METHOD_NOT_FOUND, `Unknown: ${message.method}`);
|
|
847
|
+
return void 0;
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
const aid = currentId ?? message.params["agentId"] ?? "unknown";
|
|
851
|
+
const result = await handler(aid, message.params, message.id, socket);
|
|
852
|
+
if (message.id !== void 0) this.sendResult(socket, message.id, result);
|
|
853
|
+
if (message.method === "agent.register") return message.params["agentId"];
|
|
854
|
+
} catch (e) {
|
|
855
|
+
this.sendError(socket, message.id, RPC_INTERNAL_ERROR, e instanceof Error ? e.message : String(e));
|
|
856
|
+
}
|
|
857
|
+
return void 0;
|
|
858
|
+
}
|
|
859
|
+
// ── Default Handlers ──────────────────────────────────────────────
|
|
860
|
+
registerDefaultHandlers() {
|
|
861
|
+
this.handlers.set("agent.register", async (_cid, params, _id, socket) => {
|
|
862
|
+
const agentId = params["agentId"];
|
|
863
|
+
const agentName = params["agentName"];
|
|
864
|
+
if (!agentId || !agentName) throw new IPCError("agent.register requires agentId and agentName");
|
|
865
|
+
this.registerClientSocket(agentId, agentName, socket);
|
|
866
|
+
logger.info({ agentId, agentName }, "Agent registered");
|
|
867
|
+
return { registered: true, agentId };
|
|
868
|
+
});
|
|
869
|
+
this.handlers.set("agent.streamChunk", async (_cid, params) => {
|
|
870
|
+
getEventBus().emit("model:stream:chunk", {
|
|
871
|
+
model: params["model"] ?? "unknown",
|
|
872
|
+
content: params["content"] ?? ""
|
|
873
|
+
});
|
|
874
|
+
return { received: true };
|
|
875
|
+
});
|
|
876
|
+
this.handlers.set("agent.taskUpdate", async (_cid, params) => {
|
|
877
|
+
const taskId = params["taskId"];
|
|
878
|
+
const status = params["status"];
|
|
879
|
+
if (taskId && status) getEventBus().emit("task:updated", { taskId, status });
|
|
880
|
+
return { received: true };
|
|
881
|
+
});
|
|
882
|
+
this.handlers.set("agent.message", async (_cid, params) => {
|
|
883
|
+
const to = params["to"];
|
|
884
|
+
const content = params["content"];
|
|
885
|
+
const from = params["from"];
|
|
886
|
+
if (from && to && content) {
|
|
887
|
+
getEventBus().emit("agent:message", { from, to, content });
|
|
888
|
+
const target = this.clients.get(to);
|
|
889
|
+
if (target) {
|
|
890
|
+
this.write(target.socket, { jsonrpc: "2.0", method: "agent.message", params: { from, content } });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return { delivered: this.clients.has(to ?? "") };
|
|
894
|
+
});
|
|
895
|
+
this.handlers.set("hub.taskAssign", async (_cid, params) => {
|
|
896
|
+
const agentId = params["agentId"];
|
|
897
|
+
const taskId = params["taskId"];
|
|
898
|
+
if (agentId && taskId) {
|
|
899
|
+
const target = this.clients.get(agentId);
|
|
900
|
+
if (target) this.write(target.socket, { jsonrpc: "2.0", method: "hub.taskAssign", params: { taskId, ...params } });
|
|
901
|
+
}
|
|
902
|
+
return { assigned: this.clients.has(agentId ?? "") };
|
|
903
|
+
});
|
|
904
|
+
this.handlers.set("hub.shutdown", async (_cid, params) => {
|
|
905
|
+
const agentId = params["agentId"];
|
|
906
|
+
if (agentId) {
|
|
907
|
+
const target = this.clients.get(agentId);
|
|
908
|
+
if (target) {
|
|
909
|
+
this.write(target.socket, {
|
|
910
|
+
jsonrpc: "2.0",
|
|
911
|
+
method: "hub.shutdown",
|
|
912
|
+
params: { reason: params["reason"] ?? "Shutdown requested" }
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return { notified: this.clients.has(agentId ?? "") };
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
// ── HMAC ──────────────────────────────────────────────────────────
|
|
920
|
+
verifyHmac(message, hmac) {
|
|
921
|
+
const expected = this.computeHmac(message);
|
|
922
|
+
if (expected.length !== hmac.length) return false;
|
|
923
|
+
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(hmac, "hex"));
|
|
924
|
+
}
|
|
925
|
+
isAuthEnvelope(v) {
|
|
926
|
+
if (typeof v !== "object" || v === null) return false;
|
|
927
|
+
const o = v;
|
|
928
|
+
return typeof o["hmac"] === "string" && typeof o["message"] === "object" && o["message"] !== null;
|
|
929
|
+
}
|
|
930
|
+
// ── I/O ───────────────────────────────────────────────────────────
|
|
931
|
+
write(socket, msg) {
|
|
932
|
+
if (!socket.destroyed) socket.write(JSON.stringify(msg) + "\n");
|
|
933
|
+
}
|
|
934
|
+
sendResult(socket, id, result) {
|
|
935
|
+
this.write(socket, { jsonrpc: "2.0", result, id });
|
|
936
|
+
}
|
|
937
|
+
sendError(socket, id, code, message) {
|
|
938
|
+
this.write(socket, { jsonrpc: "2.0", error: { code, message }, id: id ?? 0 });
|
|
939
|
+
}
|
|
940
|
+
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
941
|
+
async removeStaleSocket() {
|
|
942
|
+
try {
|
|
943
|
+
await unlink(this.socketPath);
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
setupProcessCleanup() {
|
|
948
|
+
const cleanup = () => {
|
|
949
|
+
this.destroy().catch(() => {
|
|
950
|
+
});
|
|
951
|
+
};
|
|
952
|
+
process.once("exit", cleanup);
|
|
953
|
+
process.once("SIGINT", cleanup);
|
|
954
|
+
process.once("SIGTERM", cleanup);
|
|
955
|
+
}
|
|
956
|
+
assertAlive() {
|
|
957
|
+
if (this.disposed) throw new IPCError("IPCHub has been disposed");
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
961
|
+
var DEFAULT_RECONNECT_RETRIES = 5;
|
|
962
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
963
|
+
var NEWLINE2 = 10;
|
|
964
|
+
var PaneProcess = class {
|
|
965
|
+
agentId;
|
|
966
|
+
agentName;
|
|
967
|
+
socketPath;
|
|
968
|
+
hmacSecret;
|
|
969
|
+
heartbeatMs;
|
|
970
|
+
maxRetries;
|
|
971
|
+
msgHandlers = /* @__PURE__ */ new Map();
|
|
972
|
+
pending = /* @__PURE__ */ new Map();
|
|
973
|
+
socket;
|
|
974
|
+
heartbeatTimer;
|
|
975
|
+
nextId = 1;
|
|
976
|
+
connected = false;
|
|
977
|
+
disposed = false;
|
|
978
|
+
constructor(options) {
|
|
979
|
+
this.agentId = options.agentId;
|
|
980
|
+
this.agentName = options.agentName;
|
|
981
|
+
this.socketPath = options.socketPath;
|
|
982
|
+
this.hmacSecret = options.hmacSecret;
|
|
983
|
+
this.heartbeatMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
984
|
+
this.maxRetries = options.reconnectMaxRetries ?? DEFAULT_RECONNECT_RETRIES;
|
|
985
|
+
}
|
|
986
|
+
/** Connect to the IPC hub and register this agent. */
|
|
987
|
+
async connect() {
|
|
988
|
+
this.assertAlive();
|
|
989
|
+
await this.establish();
|
|
990
|
+
await this.register();
|
|
991
|
+
this.startHeartbeat();
|
|
992
|
+
logger.info({ agentId: this.agentId }, "PaneProcess connected");
|
|
993
|
+
}
|
|
994
|
+
/** Send a JSON-RPC 2.0 request and wait for response. */
|
|
995
|
+
async request(method, params) {
|
|
996
|
+
this.assertAlive();
|
|
997
|
+
this.assertConnected();
|
|
998
|
+
const id = this.nextId++;
|
|
999
|
+
const message = { jsonrpc: "2.0", method, params: { ...params, agentId: this.agentId }, id };
|
|
1000
|
+
return new Promise((resolve, reject) => {
|
|
1001
|
+
const timer = setTimeout(() => {
|
|
1002
|
+
this.pending.delete(id);
|
|
1003
|
+
reject(new IPCError(`Request timeout: ${method}`));
|
|
1004
|
+
}, REQUEST_TIMEOUT_MS);
|
|
1005
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
1006
|
+
this.sendAuthenticated(message);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
/** Send a notification (no response expected). */
|
|
1010
|
+
notify(method, params) {
|
|
1011
|
+
this.assertAlive();
|
|
1012
|
+
this.assertConnected();
|
|
1013
|
+
this.sendAuthenticated({ jsonrpc: "2.0", method, params: { ...params, agentId: this.agentId } });
|
|
1014
|
+
}
|
|
1015
|
+
/** Convenience: send a stream chunk. */
|
|
1016
|
+
sendStreamChunk(content, model, taskId) {
|
|
1017
|
+
this.notify("agent.streamChunk", { content, model, taskId });
|
|
1018
|
+
}
|
|
1019
|
+
/** Convenience: send a task status update. */
|
|
1020
|
+
sendTaskUpdate(taskId, status) {
|
|
1021
|
+
this.notify("agent.taskUpdate", { taskId, status });
|
|
1022
|
+
}
|
|
1023
|
+
/** Convenience: send a message to another agent via the hub. */
|
|
1024
|
+
async sendMessage(to, content) {
|
|
1025
|
+
return this.request("agent.message", { from: this.agentId, to, content });
|
|
1026
|
+
}
|
|
1027
|
+
/** Register a handler for incoming messages of a specific method. */
|
|
1028
|
+
onMessage(method, handler) {
|
|
1029
|
+
const set = this.msgHandlers.get(method) ?? /* @__PURE__ */ new Set();
|
|
1030
|
+
set.add(handler);
|
|
1031
|
+
this.msgHandlers.set(method, set);
|
|
1032
|
+
return () => {
|
|
1033
|
+
set.delete(handler);
|
|
1034
|
+
if (set.size === 0) this.msgHandlers.delete(method);
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
isConnected() {
|
|
1038
|
+
return this.connected && !this.disposed;
|
|
1039
|
+
}
|
|
1040
|
+
/** Gracefully disconnect from the hub. */
|
|
1041
|
+
async disconnect() {
|
|
1042
|
+
if (this.disposed) return;
|
|
1043
|
+
this.disposed = true;
|
|
1044
|
+
this.stopHeartbeat();
|
|
1045
|
+
this.rejectAll();
|
|
1046
|
+
if (this.socket) {
|
|
1047
|
+
this.socket.end();
|
|
1048
|
+
this.socket = void 0;
|
|
1049
|
+
}
|
|
1050
|
+
this.connected = false;
|
|
1051
|
+
logger.info({ agentId: this.agentId }, "PaneProcess disconnected");
|
|
1052
|
+
}
|
|
1053
|
+
// ── Connection ────────────────────────────────────────────────────
|
|
1054
|
+
async establish() {
|
|
1055
|
+
await withRetry(() => this.connectSocket(), {
|
|
1056
|
+
maxRetries: this.maxRetries,
|
|
1057
|
+
baseDelayMs: 500,
|
|
1058
|
+
maxDelayMs: 1e4,
|
|
1059
|
+
shouldRetry: () => !this.disposed
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
connectSocket() {
|
|
1063
|
+
return new Promise((resolve, reject) => {
|
|
1064
|
+
const socket = connect(this.socketPath);
|
|
1065
|
+
let buffer = Buffer.alloc(0);
|
|
1066
|
+
socket.on("connect", () => {
|
|
1067
|
+
this.socket = socket;
|
|
1068
|
+
this.connected = true;
|
|
1069
|
+
resolve();
|
|
1070
|
+
});
|
|
1071
|
+
socket.on("data", (data) => {
|
|
1072
|
+
buffer = Buffer.concat([buffer, data]);
|
|
1073
|
+
let idx;
|
|
1074
|
+
while ((idx = buffer.indexOf(NEWLINE2)) !== -1) {
|
|
1075
|
+
const raw = buffer.subarray(0, idx).toString("utf-8");
|
|
1076
|
+
buffer = buffer.subarray(idx + 1);
|
|
1077
|
+
if (raw.length > 0) this.handleIncoming(raw);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
socket.on("close", () => {
|
|
1081
|
+
this.connected = false;
|
|
1082
|
+
if (!this.disposed) {
|
|
1083
|
+
logger.warn({ agentId: this.agentId }, "Connection lost, reconnecting");
|
|
1084
|
+
this.attemptReconnect();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
socket.on("error", (err) => {
|
|
1088
|
+
if (!this.connected) reject(new IPCError(`Connection failed: ${err.message}`));
|
|
1089
|
+
else logger.error({ agentId: this.agentId, error: err.message }, "Socket error");
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
async attemptReconnect() {
|
|
1094
|
+
if (this.disposed) return;
|
|
1095
|
+
this.stopHeartbeat();
|
|
1096
|
+
try {
|
|
1097
|
+
await sleep(1e3);
|
|
1098
|
+
await this.establish();
|
|
1099
|
+
await this.register();
|
|
1100
|
+
this.startHeartbeat();
|
|
1101
|
+
logger.info({ agentId: this.agentId }, "Reconnected");
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
logger.error({ agentId: this.agentId, error: e instanceof Error ? e.message : String(e) }, "Reconnect failed");
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async register() {
|
|
1107
|
+
await this.request("agent.register", { agentId: this.agentId, agentName: this.agentName });
|
|
1108
|
+
}
|
|
1109
|
+
// ── Message Processing ────────────────────────────────────────────
|
|
1110
|
+
handleIncoming(raw) {
|
|
1111
|
+
let parsed;
|
|
1112
|
+
try {
|
|
1113
|
+
parsed = JSON.parse(raw);
|
|
1114
|
+
} catch {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (this.isResponse(parsed)) {
|
|
1118
|
+
this.resolveResponse(parsed);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (this.isMessage(parsed)) {
|
|
1122
|
+
this.dispatch(parsed);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
resolveResponse(response) {
|
|
1127
|
+
const p = this.pending.get(response.id);
|
|
1128
|
+
if (!p) return;
|
|
1129
|
+
this.pending.delete(response.id);
|
|
1130
|
+
clearTimeout(p.timer);
|
|
1131
|
+
if (response.error) p.reject(new IPCError(`RPC ${response.error.code}: ${response.error.message}`));
|
|
1132
|
+
else p.resolve(response.result);
|
|
1133
|
+
}
|
|
1134
|
+
dispatch(message) {
|
|
1135
|
+
const handlers = this.msgHandlers.get(message.method);
|
|
1136
|
+
if (!handlers) return;
|
|
1137
|
+
for (const h of handlers) {
|
|
1138
|
+
try {
|
|
1139
|
+
h(message);
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
logger.error({ method: message.method, error: e instanceof Error ? e.message : String(e) }, "Handler error");
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// ── Heartbeat ─────────────────────────────────────────────────────
|
|
1146
|
+
startHeartbeat() {
|
|
1147
|
+
this.stopHeartbeat();
|
|
1148
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1149
|
+
if (this.connected && this.socket && !this.socket.destroyed) {
|
|
1150
|
+
this.notify("agent.streamChunk", { content: "", model: "heartbeat", heartbeat: true });
|
|
1151
|
+
}
|
|
1152
|
+
}, this.heartbeatMs);
|
|
1153
|
+
if (this.heartbeatTimer.unref) this.heartbeatTimer.unref();
|
|
1154
|
+
}
|
|
1155
|
+
stopHeartbeat() {
|
|
1156
|
+
if (this.heartbeatTimer) {
|
|
1157
|
+
clearInterval(this.heartbeatTimer);
|
|
1158
|
+
this.heartbeatTimer = void 0;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// ── HMAC ──────────────────────────────────────────────────────────
|
|
1162
|
+
sendAuthenticated(message) {
|
|
1163
|
+
if (!this.socket || this.socket.destroyed) return;
|
|
1164
|
+
const hmac = createHmac("sha256", this.hmacSecret).update(JSON.stringify(message)).digest("hex");
|
|
1165
|
+
this.socket.write(JSON.stringify({ message, hmac }) + "\n");
|
|
1166
|
+
}
|
|
1167
|
+
// ── Type Guards ───────────────────────────────────────────────────
|
|
1168
|
+
isResponse(v) {
|
|
1169
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1170
|
+
const o = v;
|
|
1171
|
+
return o["jsonrpc"] === "2.0" && typeof o["id"] === "number" && !("method" in o);
|
|
1172
|
+
}
|
|
1173
|
+
isMessage(v) {
|
|
1174
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1175
|
+
const o = v;
|
|
1176
|
+
return o["jsonrpc"] === "2.0" && typeof o["method"] === "string";
|
|
1177
|
+
}
|
|
1178
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
1179
|
+
rejectAll() {
|
|
1180
|
+
const err = new IPCError("PaneProcess disconnected");
|
|
1181
|
+
for (const [, p] of this.pending) {
|
|
1182
|
+
clearTimeout(p.timer);
|
|
1183
|
+
p.reject(err);
|
|
1184
|
+
}
|
|
1185
|
+
this.pending.clear();
|
|
1186
|
+
}
|
|
1187
|
+
assertAlive() {
|
|
1188
|
+
if (this.disposed) throw new IPCError("PaneProcess disposed");
|
|
1189
|
+
}
|
|
1190
|
+
assertConnected() {
|
|
1191
|
+
if (!this.connected || !this.socket) throw new IPCError("Not connected");
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
// src/mcp/tool-bridge.ts
|
|
1196
|
+
var NAMESPACE_SEPARATOR = "__";
|
|
1197
|
+
var MCP_PREFIX = "mcp";
|
|
1198
|
+
var MCP_CATEGORY = "mcp";
|
|
1199
|
+
function buildToolName(serverName, toolName) {
|
|
1200
|
+
return `${MCP_PREFIX}${NAMESPACE_SEPARATOR}${serverName}${NAMESPACE_SEPARATOR}${toolName}`;
|
|
1201
|
+
}
|
|
1202
|
+
function parseToolName(namespacedName) {
|
|
1203
|
+
const parts = namespacedName.split(NAMESPACE_SEPARATOR);
|
|
1204
|
+
if (parts.length < 3 || parts[0] !== MCP_PREFIX) {
|
|
1205
|
+
return void 0;
|
|
1206
|
+
}
|
|
1207
|
+
const serverName = parts[1];
|
|
1208
|
+
const toolName = parts.slice(2).join(NAMESPACE_SEPARATOR);
|
|
1209
|
+
if (!serverName || !toolName) {
|
|
1210
|
+
return void 0;
|
|
1211
|
+
}
|
|
1212
|
+
return { serverName, toolName };
|
|
1213
|
+
}
|
|
1214
|
+
function convertInputSchema(inputSchema) {
|
|
1215
|
+
const properties = inputSchema["properties"];
|
|
1216
|
+
if (!properties || typeof properties !== "object") {
|
|
1217
|
+
return [];
|
|
1218
|
+
}
|
|
1219
|
+
const requiredList = inputSchema["required"];
|
|
1220
|
+
const requiredSet = new Set(
|
|
1221
|
+
Array.isArray(requiredList) ? requiredList : []
|
|
1222
|
+
);
|
|
1223
|
+
const params = [];
|
|
1224
|
+
for (const [name, rawSchema] of Object.entries(
|
|
1225
|
+
properties
|
|
1226
|
+
)) {
|
|
1227
|
+
const schema = rawSchema;
|
|
1228
|
+
if (!schema || typeof schema !== "object") {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
const param = {
|
|
1232
|
+
name,
|
|
1233
|
+
type: schema.type ?? "string",
|
|
1234
|
+
description: schema.description ?? "",
|
|
1235
|
+
required: requiredSet.has(name),
|
|
1236
|
+
...schema.default !== void 0 ? { default: schema.default } : {},
|
|
1237
|
+
...schema.enum !== void 0 ? { enum: schema.enum } : {}
|
|
1238
|
+
};
|
|
1239
|
+
params.push(param);
|
|
1240
|
+
}
|
|
1241
|
+
return params;
|
|
1242
|
+
}
|
|
1243
|
+
function mcpToolToDefinition(serverName, mcpTool) {
|
|
1244
|
+
return {
|
|
1245
|
+
name: buildToolName(serverName, mcpTool.name),
|
|
1246
|
+
description: `[MCP:${serverName}] ${mcpTool.description}`,
|
|
1247
|
+
parameters: convertInputSchema(mcpTool.inputSchema)
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function toolCallToMCPRequest(call) {
|
|
1251
|
+
const parsed = parseToolName(call.name);
|
|
1252
|
+
if (!parsed) {
|
|
1253
|
+
throw new ToolCallError(
|
|
1254
|
+
call.name,
|
|
1255
|
+
"Invalid MCP tool name format \u2014 expected mcp__{server}__{tool}"
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
serverName: parsed.serverName,
|
|
1260
|
+
toolName: parsed.toolName,
|
|
1261
|
+
arguments: call.arguments
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function mcpResultToToolResult(toolCallId, toolName, content, isError) {
|
|
1265
|
+
return { toolCallId, name: toolName, content, isError };
|
|
1266
|
+
}
|
|
1267
|
+
function validateArguments(args, parameters) {
|
|
1268
|
+
for (const param of parameters) {
|
|
1269
|
+
if (param.required && !(param.name in args)) {
|
|
1270
|
+
return `Missing required argument: ${param.name}`;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
var MCPToolBridge = class {
|
|
1276
|
+
serverManager;
|
|
1277
|
+
toolCache = /* @__PURE__ */ new Map();
|
|
1278
|
+
constructor(serverManager) {
|
|
1279
|
+
this.serverManager = serverManager;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Discover tools from all connected MCP servers and register them.
|
|
1283
|
+
* Uses lazy loading: definitions are fetched and cached on first request.
|
|
1284
|
+
*/
|
|
1285
|
+
async registerAll(registry) {
|
|
1286
|
+
const servers = this.serverManager.getConnectedServers();
|
|
1287
|
+
let totalRegistered = 0;
|
|
1288
|
+
for (const serverName of servers) {
|
|
1289
|
+
try {
|
|
1290
|
+
const count = await this.registerServerTools(serverName, registry);
|
|
1291
|
+
totalRegistered += count;
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1294
|
+
logger.error(
|
|
1295
|
+
{ server: serverName, error: msg },
|
|
1296
|
+
"Failed to register MCP tools"
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
logger.info(
|
|
1301
|
+
{ totalRegistered, servers: servers.length },
|
|
1302
|
+
"MCP tool registration complete"
|
|
1303
|
+
);
|
|
1304
|
+
return totalRegistered;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Register tools from a single MCP server.
|
|
1308
|
+
* Fetches tool schemas and caches definitions.
|
|
1309
|
+
*/
|
|
1310
|
+
async registerServerTools(serverName, registry) {
|
|
1311
|
+
const definitions = await this.loadToolDefinitions(serverName);
|
|
1312
|
+
for (const definition of definitions) {
|
|
1313
|
+
const registration = this.createRegistration(serverName, definition);
|
|
1314
|
+
registry.register(registration);
|
|
1315
|
+
}
|
|
1316
|
+
logger.info(
|
|
1317
|
+
{ server: serverName, tools: definitions.length },
|
|
1318
|
+
"Registered MCP server tools"
|
|
1319
|
+
);
|
|
1320
|
+
return definitions.length;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Lazy-load tool definitions for a server.
|
|
1324
|
+
* Returns cached definitions if already loaded.
|
|
1325
|
+
*/
|
|
1326
|
+
async loadToolDefinitions(serverName) {
|
|
1327
|
+
const cached = this.toolCache.get(serverName);
|
|
1328
|
+
if (cached) {
|
|
1329
|
+
return cached;
|
|
1330
|
+
}
|
|
1331
|
+
const mcpTools = await this.serverManager.listServerTools(serverName);
|
|
1332
|
+
const definitions = mcpTools.map(
|
|
1333
|
+
(tool) => mcpToolToDefinition(serverName, tool)
|
|
1334
|
+
);
|
|
1335
|
+
this.toolCache.set(serverName, definitions);
|
|
1336
|
+
return definitions;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Execute an MCP tool call: validate, convert, call, return result.
|
|
1340
|
+
*/
|
|
1341
|
+
async executeTool(call) {
|
|
1342
|
+
const request = toolCallToMCPRequest(call);
|
|
1343
|
+
const definitions = this.toolCache.get(request.serverName);
|
|
1344
|
+
if (definitions) {
|
|
1345
|
+
const def = definitions.find((d) => d.name === call.name);
|
|
1346
|
+
if (def) {
|
|
1347
|
+
const validationError = validateArguments(call.arguments, def.parameters);
|
|
1348
|
+
if (validationError) {
|
|
1349
|
+
return mcpResultToToolResult(call.id, call.name, validationError, true);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
this.serverManager.checkRateLimitFor(request.serverName);
|
|
1354
|
+
this.serverManager.recordCall(request.serverName);
|
|
1355
|
+
const client = this.serverManager.getClient(request.serverName);
|
|
1356
|
+
if (!client) {
|
|
1357
|
+
return mcpResultToToolResult(
|
|
1358
|
+
call.id,
|
|
1359
|
+
call.name,
|
|
1360
|
+
`MCP server "${request.serverName}" is not connected`,
|
|
1361
|
+
true
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
const result = await client.callTool(request.toolName, request.arguments);
|
|
1366
|
+
return mcpResultToToolResult(
|
|
1367
|
+
call.id,
|
|
1368
|
+
call.name,
|
|
1369
|
+
result.content,
|
|
1370
|
+
result.isError
|
|
1371
|
+
);
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1374
|
+
return mcpResultToToolResult(call.id, call.name, msg, true);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/** Invalidate cached tool definitions for a server (e.g. after config reload). */
|
|
1378
|
+
invalidateCache(serverName) {
|
|
1379
|
+
if (serverName) {
|
|
1380
|
+
this.toolCache.delete(serverName);
|
|
1381
|
+
} else {
|
|
1382
|
+
this.toolCache.clear();
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
/** Check whether a tool name is an MCP-namespaced tool. */
|
|
1386
|
+
isMCPTool(toolName) {
|
|
1387
|
+
return parseToolName(toolName) !== void 0;
|
|
1388
|
+
}
|
|
1389
|
+
/** Get all cached tool definitions across all servers. */
|
|
1390
|
+
getAllDefinitions() {
|
|
1391
|
+
const all = [];
|
|
1392
|
+
for (const defs of this.toolCache.values()) {
|
|
1393
|
+
all.push(...defs);
|
|
1394
|
+
}
|
|
1395
|
+
return all;
|
|
1396
|
+
}
|
|
1397
|
+
// ── Private ─────────────────────────────────────────────────────────
|
|
1398
|
+
createRegistration(_serverName, definition) {
|
|
1399
|
+
return {
|
|
1400
|
+
definition,
|
|
1401
|
+
category: MCP_CATEGORY,
|
|
1402
|
+
requiresApproval: (mode, _args) => {
|
|
1403
|
+
return mode === "strict";
|
|
1404
|
+
},
|
|
1405
|
+
execute: async (args) => {
|
|
1406
|
+
const call = {
|
|
1407
|
+
id: `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1408
|
+
name: definition.name,
|
|
1409
|
+
arguments: args
|
|
1410
|
+
};
|
|
1411
|
+
return this.executeTool(call);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
export { ConversationStore, IPCHub, ITerm2Manager, MCPToolBridge, PaneProcess, SqliteStore };
|
|
1418
|
+
//# sourceMappingURL=index.js.map
|
|
1419
|
+
//# sourceMappingURL=index.js.map
|