feat(mitm): add detection modules for 8 agents (F3)

Filesystem-only installation probes for antigravity, kiro, copilot,
codex, cursor, zed, claude-code, and open-code. detectAgent(id)
dispatcher returns {installed, path?} without ever spawning a shell
or interpolating runtime paths (Hard Rule #13).

Trae has no detector — its entry in the dispatch table returns
{installed: false} until upstream viability is confirmed.
This commit is contained in:
diegosouzapw
2026-05-27 21:31:46 -03:00
parent 304dcac4cc
commit 6347cbfe5d
9 changed files with 302 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
/**
* Antigravity IDE installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
// macOS
"/Applications/Antigravity.app",
path.join(HOME, "Applications", "Antigravity.app"),
// Linux (AppImage / system install)
"/usr/bin/antigravity",
"/usr/local/bin/antigravity",
path.join(HOME, ".local", "bin", "antigravity"),
// Windows
path.join(
process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"),
"Programs",
"Antigravity",
"Antigravity.exe"
),
];
export function detectAntigravity(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}

View File

@@ -0,0 +1,29 @@
/**
* Claude Code (Anthropic CLI) installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
"/usr/local/bin/claude",
"/usr/bin/claude",
path.join(HOME, ".local", "bin", "claude"),
path.join(HOME, ".npm-global", "bin", "claude"),
path.join(HOME, ".claude"),
path.join(
process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"),
"npm",
"claude.cmd"
),
];
export function detectClaudeCode(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}

View File

@@ -0,0 +1,29 @@
/**
* OpenAI Codex CLI installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
"/usr/local/bin/codex",
"/usr/bin/codex",
path.join(HOME, ".local", "bin", "codex"),
path.join(HOME, ".npm-global", "bin", "codex"),
path.join(HOME, "node_modules", ".bin", "codex"),
path.join(
process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"),
"npm",
"codex.cmd"
),
];
export function detectCodex(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}

View File

@@ -0,0 +1,39 @@
/**
* GitHub Copilot installation detection.
*
* Detection strategy: look for the Copilot extension folder inside the user's
* VS Code (or fork) extensions directory. Purely filesystem-based — no shell
* interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const EXTENSIONS_DIRS = [
path.join(HOME, ".vscode", "extensions"),
path.join(HOME, ".vscode-insiders", "extensions"),
path.join(HOME, ".cursor", "extensions"),
];
export function detectCopilot(): DetectionResult {
for (const dir of EXTENSIONS_DIRS) {
try {
if (!fs.existsSync(dir)) continue;
const entries = fs.readdirSync(dir);
for (const name of entries) {
// Copilot extensions are named like `github.copilot-1.x.x`,
// `github.copilot-chat-...`. Match by prefix only.
const lower = name.toLowerCase();
if (lower.startsWith("github.copilot")) {
return { installed: true, path: path.join(dir, name) };
}
}
} catch {
// Permission or transient fs error — skip this directory.
}
}
return { installed: false };
}

View File

@@ -0,0 +1,31 @@
/**
* Cursor IDE installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
"/Applications/Cursor.app",
path.join(HOME, "Applications", "Cursor.app"),
"/usr/bin/cursor",
"/usr/local/bin/cursor",
path.join(HOME, ".local", "bin", "cursor"),
path.join(HOME, ".cursor"),
path.join(
process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"),
"Programs",
"cursor",
"Cursor.exe"
),
];
export function detectCursor(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}

View File

@@ -0,0 +1,53 @@
/**
* Detection dispatcher.
*
* `detectAgent(id)` returns whether the given AgentBridge target is installed
* on the current machine. All detection probes are filesystem-only — they
* never spawn shells or interpolate runtime paths (Hard Rule #13).
*
* Trae is intentionally absent from the dispatch table: its viability is still
* under investigation, so callers receive `{ installed: false }` until the
* upstream surface is confirmed (see `targets/trae.ts`).
*/
import type { AgentId, DetectionResult } from "../types";
import { detectAntigravity } from "./antigravity";
import { detectKiro } from "./kiro";
import { detectCopilot } from "./copilot";
import { detectCodex } from "./codex";
import { detectCursor } from "./cursor";
import { detectZed } from "./zed";
import { detectClaudeCode } from "./claudeCode";
import { detectOpenCode } from "./openCode";
export const DETECTORS: Record<AgentId, () => DetectionResult> = {
antigravity: detectAntigravity,
kiro: detectKiro,
copilot: detectCopilot,
codex: detectCodex,
cursor: detectCursor,
zed: detectZed,
"claude-code": detectClaudeCode,
"open-code": detectOpenCode,
trae: () => ({ installed: false }),
};
export function detectAgent(id: AgentId): DetectionResult {
const fn = DETECTORS[id];
if (!fn) return { installed: false };
try {
return fn();
} catch {
return { installed: false };
}
}
export {
detectAntigravity,
detectKiro,
detectCopilot,
detectCodex,
detectCursor,
detectZed,
detectClaudeCode,
detectOpenCode,
};

View File

@@ -0,0 +1,30 @@
/**
* Kiro IDE installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
"/Applications/Kiro.app",
path.join(HOME, "Applications", "Kiro.app"),
"/usr/bin/kiro",
"/usr/local/bin/kiro",
path.join(HOME, ".local", "bin", "kiro"),
path.join(
process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"),
"Programs",
"Kiro",
"Kiro.exe"
),
];
export function detectKiro(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}

View File

@@ -0,0 +1,32 @@
/**
* OpenCode installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
"/Applications/OpenCode.app",
path.join(HOME, "Applications", "OpenCode.app"),
"/usr/bin/opencode",
"/usr/local/bin/opencode",
path.join(HOME, ".local", "bin", "opencode"),
path.join(HOME, ".opencode"),
path.join(HOME, ".config", "opencode"),
path.join(
process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"),
"Programs",
"OpenCode",
"OpenCode.exe"
),
];
export function detectOpenCode(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}

26
src/mitm/detection/zed.ts Normal file
View File

@@ -0,0 +1,26 @@
/**
* Zed editor installation detection.
* Purely filesystem-based — no shell interpolation (Hard Rule #13).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { DetectionResult } from "../types";
const HOME = os.homedir();
const PATHS = [
"/Applications/Zed.app",
path.join(HOME, "Applications", "Zed.app"),
"/usr/bin/zed",
"/usr/local/bin/zed",
path.join(HOME, ".local", "bin", "zed"),
path.join(HOME, ".local", "share", "zed"),
path.join(HOME, ".config", "zed"),
];
export function detectZed(): DetectionResult {
for (const p of PATHS) {
if (fs.existsSync(p)) return { installed: true, path: p };
}
return { installed: false };
}