androjack-mcp 1.3.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.
Files changed (70) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +34 -0
  3. package/.github/pull_request_template.md +16 -0
  4. package/CONTRIBUTING.md +27 -0
  5. package/LICENSE +21 -0
  6. package/README.md +592 -0
  7. package/SECURITY.md +26 -0
  8. package/assets/AndroJack banner.png +0 -0
  9. package/assets/killer_argument.png +0 -0
  10. package/build/constants.js +412 -0
  11. package/build/http-server.js +163 -0
  12. package/build/http.js +151 -0
  13. package/build/index.js +553 -0
  14. package/build/install.js +379 -0
  15. package/build/logger.js +57 -0
  16. package/build/tools/api-level.js +170 -0
  17. package/build/tools/api36-compliance.js +282 -0
  18. package/build/tools/architecture.js +75 -0
  19. package/build/tools/build-publish.js +362 -0
  20. package/build/tools/component.js +90 -0
  21. package/build/tools/debugger.js +82 -0
  22. package/build/tools/gradle.js +234 -0
  23. package/build/tools/kmp.js +348 -0
  24. package/build/tools/kotlin-patterns.js +500 -0
  25. package/build/tools/large-screen.js +366 -0
  26. package/build/tools/m3-expressive.js +447 -0
  27. package/build/tools/navigation3.js +331 -0
  28. package/build/tools/ondevice-ai.js +283 -0
  29. package/build/tools/permissions.js +404 -0
  30. package/build/tools/play-policy.js +221 -0
  31. package/build/tools/scalability.js +621 -0
  32. package/build/tools/search.js +89 -0
  33. package/build/tools/testing.js +439 -0
  34. package/build/tools/wear.js +337 -0
  35. package/build/tools/xr.js +274 -0
  36. package/config/antigravity_mcp.json +32 -0
  37. package/config/claude_desktop_config.json +17 -0
  38. package/config/cursor_mcp.json +21 -0
  39. package/config/jetbrains_mcp.json +28 -0
  40. package/config/kiro_mcp.json +40 -0
  41. package/config/vscode_mcp.json +24 -0
  42. package/config/windsurf_mcp.json +18 -0
  43. package/package.json +51 -0
  44. package/src/constants.ts +436 -0
  45. package/src/http-server.ts +186 -0
  46. package/src/http.ts +190 -0
  47. package/src/index.ts +702 -0
  48. package/src/install.ts +441 -0
  49. package/src/logger.ts +67 -0
  50. package/src/tools/api-level.ts +198 -0
  51. package/src/tools/api36-compliance.ts +289 -0
  52. package/src/tools/architecture.ts +94 -0
  53. package/src/tools/build-publish.ts +379 -0
  54. package/src/tools/component.ts +106 -0
  55. package/src/tools/debugger.ts +111 -0
  56. package/src/tools/gradle.ts +288 -0
  57. package/src/tools/kmp.ts +352 -0
  58. package/src/tools/kotlin-patterns.ts +534 -0
  59. package/src/tools/large-screen.ts +391 -0
  60. package/src/tools/m3-expressive.ts +473 -0
  61. package/src/tools/navigation3.ts +338 -0
  62. package/src/tools/ondevice-ai.ts +287 -0
  63. package/src/tools/permissions.ts +445 -0
  64. package/src/tools/play-policy.ts +229 -0
  65. package/src/tools/scalability.ts +646 -0
  66. package/src/tools/search.ts +112 -0
  67. package/src/tools/testing.ts +460 -0
  68. package/src/tools/wear.ts +343 -0
  69. package/src/tools/xr.ts +278 -0
  70. package/tsconfig.json +17 -0
package/src/install.ts ADDED
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AndroJack MCP – Smart Installer
4
+ *
5
+ * Detects OS, installed IDEs, and config file locations automatically.
6
+ * Supports both automated (--auto) and guided interactive installation.
7
+ *
8
+ * Usage:
9
+ * npx androjack-mcp install → interactive guided mode
10
+ * npx androjack-mcp install --auto → auto-detect and install to all found IDEs
11
+ * npx androjack-mcp install --ide cursor → target a specific IDE
12
+ * npx androjack-mcp install --list → list all supported IDEs and their status
13
+ */
14
+
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import * as os from "os";
18
+ import * as readline from "readline";
19
+
20
+ // ── Types ───────────────────────────────────────────────────────────────────
21
+
22
+ interface IdeTarget {
23
+ id: string;
24
+ name: string;
25
+ configPaths: string[]; // ordered by preference
26
+ configKey: "mcpServers" | "servers"; // JSON structure key
27
+ format: "standard" | "vscode";
28
+ oneClickUrl?: string;
29
+ notes?: string;
30
+ }
31
+
32
+ interface InstallResult {
33
+ ide: string;
34
+ success: boolean;
35
+ path?: string;
36
+ message: string;
37
+ }
38
+
39
+ // ── AndroJack server config block (reused for all IDEs) ───────────────────
40
+
41
+ const SERVER_CONFIG_STANDARD = {
42
+ command: "npx",
43
+ args: ["-y", "androjack-mcp"],
44
+ env: {},
45
+ autoApprove: [],
46
+ disabled: false,
47
+ };
48
+
49
+ const SERVER_CONFIG_VSCODE = {
50
+ type: "stdio",
51
+ command: "npx",
52
+ args: ["-y", "androjack-mcp"],
53
+ };
54
+
55
+ // ── IDE Definitions ─────────────────────────────────────────────────────────
56
+
57
+ const HOME = os.homedir();
58
+ const PLATFORM = process.platform; // darwin | linux | win32
59
+
60
+ function getConfigPaths(platform: string): IdeTarget[] {
61
+ const appdata = process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming");
62
+ const localappdata = process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local");
63
+
64
+ return [
65
+ // ── Claude Desktop ──────────────────────────────────────────────────────
66
+ {
67
+ id: "claude",
68
+ name: "Claude Desktop",
69
+ configPaths:
70
+ platform === "darwin"
71
+ ? [path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")]
72
+ : platform === "win32"
73
+ ? [path.join(appdata, "Claude", "claude_desktop_config.json")]
74
+ : [path.join(HOME, ".config", "Claude", "claude_desktop_config.json")],
75
+ configKey: "mcpServers",
76
+ format: "standard",
77
+ notes: "Restart Claude Desktop after install. Look for 🔨 in chat input.",
78
+ },
79
+
80
+ // ── Cursor IDE ──────────────────────────────────────────────────────────
81
+ {
82
+ id: "cursor",
83
+ name: "Cursor",
84
+ configPaths: [
85
+ path.join(process.cwd(), ".cursor", "mcp.json"), // project-level (preferred)
86
+ path.join(HOME, ".cursor", "mcp.json"), // global
87
+ ],
88
+ configKey: "mcpServers",
89
+ format: "standard",
90
+ notes: "Check Settings → MCP for green dot confirmation.",
91
+ },
92
+
93
+ // ── Windsurf (Codeium) ──────────────────────────────────────────────────
94
+ {
95
+ id: "windsurf",
96
+ name: "Windsurf",
97
+ configPaths: [
98
+ path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
99
+ path.join(HOME, ".windsurf", "mcp_config.json"),
100
+ ],
101
+ configKey: "mcpServers",
102
+ format: "standard",
103
+ notes: "Restart Windsurf → Cascade panel shows AndroJack tools.",
104
+ },
105
+
106
+ // ── VS Code (GitHub Copilot) ─────────────────────────────────────────────
107
+ {
108
+ id: "vscode",
109
+ name: "VS Code (GitHub Copilot)",
110
+ configPaths: [
111
+ path.join(process.cwd(), ".vscode", "mcp.json"), // workspace (preferred)
112
+ ...(platform === "darwin"
113
+ ? [path.join(HOME, "Library", "Application Support", "Code", "User", "settings.json")]
114
+ : platform === "win32"
115
+ ? [path.join(appdata, "Code", "User", "settings.json")]
116
+ : [path.join(HOME, ".config", "Code", "User", "settings.json")]),
117
+ ],
118
+ configKey: "servers",
119
+ format: "vscode",
120
+ notes: "VS Code 1.99+ required. Copilot Chat → Agent mode to access tools.",
121
+ },
122
+
123
+ // ── AWS Kiro IDE ─────────────────────────────────────────────────────────
124
+ {
125
+ id: "kiro",
126
+ name: "AWS Kiro",
127
+ configPaths: [
128
+ path.join(process.cwd(), ".kiro", "settings", "mcp.json"), // project-level
129
+ path.join(HOME, ".kiro", "settings", "mcp.json"), // global
130
+ ],
131
+ configKey: "mcpServers",
132
+ format: "standard",
133
+ oneClickUrl: (() => {
134
+ const name = encodeURIComponent("androjack");
135
+ const config = encodeURIComponent(
136
+ JSON.stringify({ command: "npx", args: ["-y", "androjack-mcp"], disabled: false, autoApprove: [] })
137
+ );
138
+ return `https://kiro.dev/launch/mcp/add?name=${name}&config=${config}`;
139
+ })(),
140
+ notes: "Or use the one-click Kiro install link in the README.",
141
+ },
142
+
143
+ // ── Google Antigravity IDE (standalone, launched Nov 18 2025 with Gemini 3) ────
144
+ // NOT Firebase Studio / Project IDX — those are separate Google products.
145
+ // Confirmed config path from real usage: ~/.gemini/antigravity/mcp_config.json
146
+ {
147
+ id: "antigravity",
148
+ name: "Google Antigravity IDE",
149
+ configPaths: [
150
+ path.join(HOME, ".gemini", "antigravity", "mcp_config.json"),
151
+ ],
152
+ configKey: "mcpServers",
153
+ format: "standard",
154
+ notes:
155
+ "After saving: Antigravity Agent pane → '...' → MCP Servers → Manage → Refresh.",
156
+ },
157
+
158
+ // ── JetBrains (Android Studio / IntelliJ) ────────────────────────────────
159
+ {
160
+ id: "jetbrains",
161
+ name: "JetBrains AI Assistant (Android Studio / IntelliJ)",
162
+ configPaths: [
163
+ // JetBrains stores MCP config inside IDE-version-specific dirs
164
+ ...(platform === "darwin"
165
+ ? [
166
+ path.join(HOME, "Library", "Application Support", "JetBrains", "AndroidStudio2024.3", "mcp.json"),
167
+ path.join(HOME, "Library", "Application Support", "JetBrains", "IdeaIC2024.3", "mcp.json"),
168
+ ]
169
+ : platform === "win32"
170
+ ? [
171
+ path.join(appdata, "JetBrains", "AndroidStudio2024.3", "mcp.json"),
172
+ path.join(appdata, "JetBrains", "IdeaIC2024.3", "mcp.json"),
173
+ ]
174
+ : [
175
+ path.join(HOME, ".config", "JetBrains", "AndroidStudio2024.3", "mcp.json"),
176
+ path.join(HOME, ".config", "JetBrains", "IdeaIC2024.3", "mcp.json"),
177
+ ]),
178
+ ],
179
+ configKey: "mcpServers",
180
+ format: "standard",
181
+ notes:
182
+ "Or add manually: Android Studio → Settings → Tools → AI Assistant → MCP Servers → +",
183
+ },
184
+ ];
185
+ }
186
+
187
+ // ── Config helpers ──────────────────────────────────────────────────────────
188
+
189
+ function buildConfig(target: IdeTarget): Record<string, unknown> {
190
+ if (target.format === "vscode") {
191
+ return { servers: { androjack: SERVER_CONFIG_VSCODE } };
192
+ }
193
+ return { mcpServers: { androjack: SERVER_CONFIG_STANDARD } };
194
+ }
195
+
196
+ function mergeConfig(existing: Record<string, unknown>, target: IdeTarget): Record<string, unknown> {
197
+ const key = target.configKey as string;
198
+ const serverBlock =
199
+ target.format === "vscode" ? SERVER_CONFIG_VSCODE : SERVER_CONFIG_STANDARD;
200
+
201
+ const existingBlock = (existing[key] as Record<string, unknown>) ?? {};
202
+ return {
203
+ ...existing,
204
+ [key]: { ...existingBlock, androjack: serverBlock },
205
+ };
206
+ }
207
+
208
+ function installToPath(configPath: string, target: IdeTarget): InstallResult {
209
+ try {
210
+ const dir = path.dirname(configPath);
211
+
212
+ // Create directory if needed
213
+ if (!fs.existsSync(dir)) {
214
+ fs.mkdirSync(dir, { recursive: true });
215
+ }
216
+
217
+ let finalConfig: Record<string, unknown>;
218
+
219
+ if (fs.existsSync(configPath)) {
220
+ const raw = fs.readFileSync(configPath, "utf-8");
221
+ let existing: Record<string, unknown> = {};
222
+ try {
223
+ existing = JSON.parse(raw) as Record<string, unknown>;
224
+ } catch {
225
+ // Corrupted JSON — back it up and overwrite
226
+ fs.writeFileSync(configPath + ".backup", raw);
227
+ }
228
+ finalConfig = mergeConfig(existing, target);
229
+ } else {
230
+ finalConfig = buildConfig(target);
231
+ }
232
+
233
+ fs.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2) + "\n", "utf-8");
234
+
235
+ return {
236
+ ide: target.name,
237
+ success: true,
238
+ path: configPath,
239
+ message: `✅ Installed to ${configPath}`,
240
+ };
241
+ } catch (err) {
242
+ return {
243
+ ide: target.name,
244
+ success: false,
245
+ message: `❌ Failed: ${err instanceof Error ? err.message : String(err)}`,
246
+ };
247
+ }
248
+ }
249
+
250
+ // ── IDE Detection ────────────────────────────────────────────────────────────
251
+
252
+ function detectInstalledIdes(targets: IdeTarget[]): IdeTarget[] {
253
+ return targets.filter((target) => {
254
+ // Check if any of the config's parent dirs exist (suggests IDE is installed)
255
+ return target.configPaths.some((p) => fs.existsSync(path.dirname(path.dirname(p))));
256
+ });
257
+ }
258
+
259
+ function alreadyInstalled(target: IdeTarget): string | null {
260
+ for (const p of target.configPaths) {
261
+ if (!fs.existsSync(p)) continue;
262
+ try {
263
+ const json = JSON.parse(fs.readFileSync(p, "utf-8")) as Record<string, unknown>;
264
+ const key = target.configKey as string;
265
+ const servers = json[key] as Record<string, unknown> | undefined;
266
+ if (servers?.["androjack"]) return p;
267
+ } catch {
268
+ // ignore
269
+ }
270
+ }
271
+ return null;
272
+ }
273
+
274
+ // ── Output helpers ────────────────────────────────────────────────────────────
275
+
276
+ const RESET = "\x1b[0m";
277
+ const BOLD = "\x1b[1m";
278
+ const GREEN = "\x1b[32m";
279
+ const YELLOW = "\x1b[33m";
280
+ const CYAN = "\x1b[36m";
281
+ const DIM = "\x1b[2m";
282
+
283
+ function banner(): void {
284
+ console.log(`
285
+ ${BOLD}${CYAN}╔══════════════════════════════════════════════════╗
286
+ ║ 🤖 AndroJack MCP — Smart Installer ║
287
+ ║ The Jack of All Android Trades ║
288
+ ╚══════════════════════════════════════════════════╝${RESET}
289
+ `);
290
+ }
291
+
292
+ function printStatus(targets: IdeTarget[]): void {
293
+ console.log(`${BOLD}IDE Installation Status:${RESET}\n`);
294
+ for (const t of targets) {
295
+ const installed = alreadyInstalled(t);
296
+ const icon = installed ? `${GREEN}✓${RESET}` : `${DIM}○${RESET}`;
297
+ const status = installed ? `${GREEN}installed${RESET} → ${DIM}${installed}${RESET}` : `${DIM}not installed${RESET}`;
298
+ console.log(` ${icon} ${BOLD}${t.name}${RESET} — ${status}`);
299
+ }
300
+ console.log();
301
+ }
302
+
303
+ // ── Prompt helper ────────────────────────────────────────────────────────────
304
+
305
+ async function prompt(question: string): Promise<string> {
306
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
307
+ return new Promise((resolve) => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
308
+ }
309
+
310
+ // ── Install to best path for a target ───────────────────────────────────────
311
+
312
+ function installTarget(target: IdeTarget): InstallResult {
313
+ // For project-level configs, use first path. For global, prefer existing or first.
314
+ const existingPath = target.configPaths.find((p) => fs.existsSync(p));
315
+ const chosenPath = existingPath ?? target.configPaths[0];
316
+ return installToPath(chosenPath, target);
317
+ }
318
+
319
+ // ── Main entry point ─────────────────────────────────────────────────────────
320
+
321
+ async function main(): Promise<void> {
322
+ const args = process.argv.slice(2);
323
+ const subcommand = args[0];
324
+
325
+ if (!subcommand || subcommand === "install") {
326
+ banner();
327
+ const targets = getConfigPaths(PLATFORM);
328
+ const autoFlag = args.includes("--auto");
329
+ const listFlag = args.includes("--list");
330
+ const ideFlag = args.find((a) => a.startsWith("--ide="))?.split("=")[1] ?? null;
331
+
332
+ // ── --list ──────────────────────────────────────────────────────────────
333
+ if (listFlag) {
334
+ printStatus(targets);
335
+ return;
336
+ }
337
+
338
+ // ── --ide=<id> ──────────────────────────────────────────────────────────
339
+ if (ideFlag) {
340
+ const target = targets.find((t) => t.id === ideFlag);
341
+ if (!target) {
342
+ console.error(`❌ Unknown IDE: "${ideFlag}". Supported: ${targets.map((t) => t.id).join(", ")}`);
343
+ process.exit(1);
344
+ }
345
+ const result = installTarget(target);
346
+ console.log(result.message);
347
+ if (result.success && target.notes) console.log(` ${DIM}→ ${target.notes}${RESET}`);
348
+ if (target.oneClickUrl) console.log(` ${YELLOW}One-click install: ${target.oneClickUrl}${RESET}`);
349
+ return;
350
+ }
351
+
352
+ // ── --auto ──────────────────────────────────────────────────────────────
353
+ if (autoFlag) {
354
+ console.log(`${BOLD}Auto-detecting installed IDEs on ${PLATFORM}...${RESET}\n`);
355
+ const detected = detectInstalledIdes(targets);
356
+
357
+ if (detected.length === 0) {
358
+ console.log(`${YELLOW}No IDEs auto-detected. Run without --auto for guided install.${RESET}`);
359
+ return;
360
+ }
361
+
362
+ console.log(`Found: ${detected.map((t) => t.name).join(", ")}\n`);
363
+ const results: InstallResult[] = [];
364
+
365
+ for (const target of detected) {
366
+ const existing = alreadyInstalled(target);
367
+ if (existing) {
368
+ console.log(`${GREEN}⏭ ${target.name}${RESET} — already installed at ${DIM}${existing}${RESET}`);
369
+ continue;
370
+ }
371
+ const result = installTarget(target);
372
+ results.push(result);
373
+ console.log(result.message);
374
+ if (result.success && target.notes) console.log(` ${DIM}→ ${target.notes}${RESET}`);
375
+ }
376
+
377
+ const successes = results.filter((r) => r.success).length;
378
+ console.log(`\n${BOLD}${GREEN}Done. ${successes} new installation(s) completed.${RESET}`);
379
+ return;
380
+ }
381
+
382
+ // ── Interactive guided mode ─────────────────────────────────────────────
383
+ console.log(`${BOLD}Platform detected:${RESET} ${PLATFORM}\n`);
384
+ printStatus(targets);
385
+
386
+ console.log(`${BOLD}Select installation mode:${RESET}`);
387
+ console.log(` ${CYAN}1${RESET} Auto-install to all detected IDEs`);
388
+ console.log(` ${CYAN}2${RESET} Choose specific IDEs`);
389
+ console.log(` ${CYAN}3${RESET} Show manual config snippets`);
390
+ console.log(` ${CYAN}q${RESET} Quit\n`);
391
+
392
+ const choice = await prompt("Your choice: ");
393
+
394
+ if (choice === "1") {
395
+ const detected = detectInstalledIdes(targets);
396
+ for (const target of detected) {
397
+ const result = installTarget(target);
398
+ console.log(result.message);
399
+ if (result.success && target.notes) console.log(` ${DIM}→ ${target.notes}${RESET}`);
400
+ }
401
+ } else if (choice === "2") {
402
+ for (let i = 0; i < targets.length; i++) {
403
+ const t = targets[i];
404
+ const installed = alreadyInstalled(t);
405
+ const status = installed ? `${GREEN}(already installed)${RESET}` : "";
406
+ console.log(` ${CYAN}${i + 1}${RESET} ${t.name} ${status}`);
407
+ }
408
+ const input = await prompt("\nEnter numbers (e.g. 1 3 5) or 'all': ");
409
+ const selected =
410
+ input.trim() === "all"
411
+ ? targets
412
+ : input
413
+ .split(/\s+/)
414
+ .map((n) => targets[parseInt(n, 10) - 1])
415
+ .filter(Boolean);
416
+
417
+ for (const target of selected) {
418
+ const result = installTarget(target);
419
+ console.log(result.message);
420
+ if (result.success && target.notes) console.log(` ${DIM}→ ${target.notes}${RESET}`);
421
+ if (target.oneClickUrl) console.log(` ${YELLOW}One-click: ${target.oneClickUrl}${RESET}`);
422
+ }
423
+ } else if (choice === "3") {
424
+ const snippet = {
425
+ mcpServers: {
426
+ androjack: SERVER_CONFIG_STANDARD,
427
+ },
428
+ };
429
+ console.log(`\n${BOLD}Paste this into your IDE's MCP config file:${RESET}\n`);
430
+ console.log(JSON.stringify(snippet, null, 2));
431
+ console.log(`\n${DIM}For VS Code .vscode/mcp.json, use the "servers" key instead of "mcpServers".${RESET}`);
432
+ } else {
433
+ console.log("Exiting.");
434
+ }
435
+ }
436
+ }
437
+
438
+ main().catch((err) => {
439
+ console.error("Installer error:", err);
440
+ process.exit(1);
441
+ });
package/src/logger.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * AndroJack MCP — Production Logger
3
+ *
4
+ * All output goes to stderr. stdout is reserved exclusively for the MCP
5
+ * JSON-RPC protocol — writing anything else there corrupts the transport.
6
+ *
7
+ * Log levels follow the standard severity ladder. In production (NODE_ENV=production)
8
+ * only WARN and above are emitted to reduce noise; in development all levels print.
9
+ */
10
+
11
+ export type LogLevel = "debug" | "info" | "warn" | "error";
12
+
13
+ const LEVELS: Record<LogLevel, number> = {
14
+ debug: 0,
15
+ info: 1,
16
+ warn: 2,
17
+ error: 3,
18
+ };
19
+
20
+ const ENV_LEVEL = (process.env.LOG_LEVEL ?? (
21
+ process.env.NODE_ENV === "production" ? "warn" : "debug"
22
+ )) as LogLevel;
23
+
24
+ const MIN_LEVEL = LEVELS[ENV_LEVEL] ?? LEVELS.debug;
25
+
26
+ function emit(level: LogLevel, message: string, meta?: Record<string, unknown>): void {
27
+ if (LEVELS[level] < MIN_LEVEL) return;
28
+
29
+ const entry: Record<string, unknown> = {
30
+ ts: new Date().toISOString(),
31
+ level,
32
+ msg: message,
33
+ };
34
+ if (meta && Object.keys(meta).length > 0) {
35
+ entry.meta = meta;
36
+ }
37
+
38
+ // JSON lines format — easy to parse with any log aggregator (Datadog, Loki, CloudWatch)
39
+ process.stderr.write(JSON.stringify(entry) + "\n");
40
+ }
41
+
42
+ export const logger = {
43
+ debug: (msg: string, meta?: Record<string, unknown>) => emit("debug", msg, meta),
44
+ info: (msg: string, meta?: Record<string, unknown>) => emit("info", msg, meta),
45
+ warn: (msg: string, meta?: Record<string, unknown>) => emit("warn", msg, meta),
46
+ error: (msg: string, meta?: Record<string, unknown>) => emit("error", msg, meta),
47
+
48
+ /**
49
+ * Wraps an async tool handler with timing + error telemetry.
50
+ * Usage: const result = await logger.timed("tool_name", () => myTool(args))
51
+ */
52
+ async timed<T>(toolName: string, fn: () => Promise<T>): Promise<T> {
53
+ const start = Date.now();
54
+ try {
55
+ const result = await fn();
56
+ emit("info", "tool_call_ok", { tool: toolName, ms: Date.now() - start });
57
+ return result;
58
+ } catch (err) {
59
+ emit("error", "tool_call_error", {
60
+ tool: toolName,
61
+ ms: Date.now() - start,
62
+ error: err instanceof Error ? err.message : String(err),
63
+ });
64
+ throw err;
65
+ }
66
+ },
67
+ };