@coworker-jp/aidr 0.0.1

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.
@@ -0,0 +1,36 @@
1
+ export const AGENT_ORDER = [
2
+ "claude", "cursor", "windsurf", "kiro",
3
+ "gemini", "codex",
4
+ "cline", "roo", "copilot", "qwen", "trae", "amazonq",
5
+ "jetbrains", "opencode", "aider", "amp", "crush", "antigravity", "continue",
6
+ "standalone",
7
+ ];
8
+
9
+ export const AGENT_REGISTRY = {
10
+ claude: () => import("./claude.mjs"),
11
+ cursor: () => import("./cursor.mjs"),
12
+ windsurf: () => import("./windsurf.mjs"),
13
+ kiro: () => import("./kiro.mjs"),
14
+ gemini: () => import("./gemini.mjs"),
15
+ codex: () => import("./codex.mjs"),
16
+ cline: () => import("./cline.mjs"),
17
+ roo: () => import("./roo.mjs"),
18
+ copilot: () => import("./copilot.mjs"),
19
+ qwen: () => import("./qwen.mjs"),
20
+ trae: () => import("./trae.mjs"),
21
+ amazonq: () => import("./amazonq.mjs"),
22
+ jetbrains: () => import("./jetbrains.mjs"),
23
+ opencode: () => import("./opencode.mjs"),
24
+ aider: () => import("./aider.mjs"),
25
+ amp: () => import("./amp.mjs"),
26
+ crush: () => import("./crush.mjs"),
27
+ antigravity: () => import("./antigravity.mjs"),
28
+ continue: () => import("./continue.mjs"),
29
+ standalone: () => import("./standalone.mjs"),
30
+ };
31
+
32
+ export async function loadAgent(name) {
33
+ const loader = AGENT_REGISTRY[name];
34
+ if (!loader) throw new Error(`unknown agent: ${name}`);
35
+ return await loader();
36
+ }
@@ -0,0 +1,5 @@
1
+ import { makeStub } from "./_stub.mjs";
2
+ const s = makeStub("jetbrains", "JetBrains AI", ".jetbrains");
3
+ export const meta = s.meta;
4
+ export const install = s.install;
5
+ export const uninstall = s.uninstall;
@@ -0,0 +1,79 @@
1
+ import path from "node:path";
2
+ import fsp from "node:fs/promises";
3
+ import { writeFileSafe, pathExists, rmdirIfEmpty } from "../fs-utils.mjs";
4
+ import * as T from "../templates.mjs";
5
+ import { downloadBase } from "../verify.mjs";
6
+
7
+ // agents/ai-scanner.json is user-visible JSON owned by us (kept with backup);
8
+ // all the .sh wrappers are deterministic installer output (no backup needed).
9
+ const AGENT_JSON_REL = ["agents", "ai-scanner.json"];
10
+ const SCRIPT_FILES = [
11
+ "env.sh",
12
+ "start_scanner.sh",
13
+ "start.sh",
14
+ "pre_shell.sh",
15
+ "pre_read.sh",
16
+ "pre_write.sh",
17
+ "post_shell.sh",
18
+ "post_read.sh",
19
+ "post_write.sh",
20
+ ];
21
+ const BIN_FILES = ["ai-scanner", "opengrep"];
22
+
23
+ export const meta = {
24
+ name: "kiro",
25
+ displayName: "Kiro CLI",
26
+ agentDir: ".kiro",
27
+ configDir: (home) => path.join(home, ".kiro"),
28
+ };
29
+
30
+ export async function install(installBase, accessKey, opts = {}) {
31
+ const { dryRun = false, env = "prod", scope = "project", home = installBase } = opts;
32
+ const base = meta.configDir(installBase);
33
+ const aidrDir = path.join(base, "coworker-ai");
34
+ const agentsDir = path.join(base, "agents");
35
+ const dlBase = downloadBase(env);
36
+ const agentJsonEntry = [path.join(agentsDir, "ai-scanner.json"), T.getKiroAgentJson(scope, home), 0o644];
37
+ const scriptEntries = [
38
+ [path.join(aidrDir, "env.sh"), T.agentEnvSh(accessKey, meta.agentDir), 0o755],
39
+ [path.join(aidrDir, "start_scanner.sh"), T.getStartScannerSh(dlBase, "kiro"), 0o755],
40
+ [path.join(aidrDir, "start.sh"), T.KIRO_START_SH, 0o755],
41
+ [path.join(aidrDir, "pre_shell.sh"), T.KIRO_PRE_SHELL_SH, 0o755],
42
+ [path.join(aidrDir, "pre_read.sh"), T.KIRO_PRE_READ_SH, 0o755],
43
+ [path.join(aidrDir, "pre_write.sh"), T.KIRO_PRE_WRITE_SH, 0o755],
44
+ [path.join(aidrDir, "post_shell.sh"), T.KIRO_POST_SHELL_SH, 0o755],
45
+ [path.join(aidrDir, "post_read.sh"), T.KIRO_POST_READ_SH, 0o755],
46
+ [path.join(aidrDir, "post_write.sh"), T.KIRO_POST_WRITE_SH, 0o755],
47
+ ];
48
+ const entries = [agentJsonEntry, ...scriptEntries];
49
+ const [jsonRes, ...scriptResults] = await Promise.all([
50
+ writeFileSafe(agentJsonEntry[0], agentJsonEntry[1], { dryRun, force: true, mode: agentJsonEntry[2] }),
51
+ ...scriptEntries.map(([p, c, mode]) => writeFileSafe(p, c, { dryRun, force: true, backup: false, mode })),
52
+ ]);
53
+ const backupPaths = [jsonRes, ...scriptResults].map((r) => r && r.backupPath).filter(Boolean);
54
+ return { installed: entries.map(([p]) => p), backupPaths };
55
+ }
56
+
57
+ export async function uninstall(installBase, opts = {}) {
58
+ const { dryRun = false } = opts;
59
+ const base = meta.configDir(installBase);
60
+ const aidrDir = path.join(base, "coworker-ai");
61
+ const agentsDir = path.join(base, "agents");
62
+ const binDir = path.join(base, "bin");
63
+ const candidateFiles = [
64
+ path.join(base, ...AGENT_JSON_REL),
65
+ ...SCRIPT_FILES.map((n) => path.join(aidrDir, n)),
66
+ ...BIN_FILES.map((n) => path.join(binDir, n)),
67
+ ];
68
+ const removedFiles = [];
69
+ for (const f of candidateFiles) {
70
+ if (await pathExists(f)) removedFiles.push(f);
71
+ }
72
+ if (dryRun) return { removedFiles, settings: { action: "none" }, dirsRemoved: [] };
73
+ await Promise.all(removedFiles.map((f) => fsp.rm(f, { force: true })));
74
+ const dirsRemoved = [];
75
+ for (const d of [aidrDir, agentsDir, binDir]) {
76
+ if (await rmdirIfEmpty(d)) dirsRemoved.push(d);
77
+ }
78
+ return { removedFiles, settings: { action: "none" }, dirsRemoved };
79
+ }
@@ -0,0 +1,5 @@
1
+ import { makeStub } from "./_stub.mjs";
2
+ const s = makeStub("opencode", "OpenCode", ".opencode");
3
+ export const meta = s.meta;
4
+ export const install = s.install;
5
+ export const uninstall = s.uninstall;
@@ -0,0 +1,5 @@
1
+ import { makeStub } from "./_stub.mjs";
2
+ const s = makeStub("qwen", "Qwen", ".qwen");
3
+ export const meta = s.meta;
4
+ export const install = s.install;
5
+ export const uninstall = s.uninstall;
@@ -0,0 +1,5 @@
1
+ import { makeStub } from "./_stub.mjs";
2
+ const s = makeStub("roo", "Roo", ".roo");
3
+ export const meta = s.meta;
4
+ export const install = s.install;
5
+ export const uninstall = s.uninstall;
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import fsp from "node:fs/promises";
3
+ import { pathExists, rmdirIfEmpty } from "../fs-utils.mjs";
4
+
5
+ const BIN_FILES = ["ai-scanner", "opengrep"];
6
+
7
+ export const meta = {
8
+ name: "standalone",
9
+ displayName: "Standalone (no AI agent)",
10
+ agentDir: ".aidr",
11
+ configDir: (home) => path.join(home, ".aidr"),
12
+ };
13
+
14
+ // Standalone mode has no hook scripts and no settings.json. The CLI's
15
+ // shared binary-fetch step drops ai-scanner + opengrep into
16
+ // ~/.aidr/bin/, and the scheduled installer wires up
17
+ // systemd/launchd/cron to invoke the binary periodically.
18
+ export async function install() {
19
+ return { installed: [], backupPath: null, backupPaths: [] };
20
+ }
21
+
22
+ export async function uninstall(installBase, opts = {}) {
23
+ const { dryRun = false } = opts;
24
+ const base = meta.configDir(installBase);
25
+ const binDir = path.join(base, "bin");
26
+
27
+ const candidateFiles = BIN_FILES.map((n) => path.join(binDir, n));
28
+ const removedFiles = [];
29
+ for (const f of candidateFiles) {
30
+ if (await pathExists(f)) removedFiles.push(f);
31
+ }
32
+
33
+ if (dryRun) {
34
+ return { removedFiles, settings: { action: "none" }, dirsRemoved: [] };
35
+ }
36
+
37
+ await Promise.all(removedFiles.map((f) => fsp.rm(f, { force: true })));
38
+ const dirsRemoved = [];
39
+ for (const d of [binDir, base]) {
40
+ if (await rmdirIfEmpty(d)) dirsRemoved.push(d);
41
+ }
42
+ return { removedFiles, settings: { action: "none" }, dirsRemoved };
43
+ }
@@ -0,0 +1,5 @@
1
+ import { makeStub } from "./_stub.mjs";
2
+ const s = makeStub("trae", "Trae", ".trae");
3
+ export const meta = s.meta;
4
+ export const install = s.install;
5
+ export const uninstall = s.uninstall;
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import fsp from "node:fs/promises";
3
+ import {
4
+ writeFileSafe,
5
+ writeJsonMerge,
6
+ unmergeJsonFile,
7
+ pathExists,
8
+ rmdirIfEmpty,
9
+ } from "../fs-utils.mjs";
10
+ import * as T from "../templates.mjs";
11
+ import {
12
+ buildWindsurfHooksJson,
13
+ mergeWindsurfHooksJson,
14
+ unmergeWindsurfHooksJson,
15
+ } from "../merge.mjs";
16
+ import { downloadBase } from "../verify.mjs";
17
+
18
+ const SCRIPT_FILES = [
19
+ "env.sh",
20
+ "start_scanner.sh",
21
+ "ensure_scanner.sh",
22
+ "pre_command.sh",
23
+ "pre_read.sh",
24
+ "pre_write.sh",
25
+ "post_read.sh",
26
+ "post_write.sh",
27
+ ];
28
+ const BIN_FILES = ["ai-scanner", "opengrep"];
29
+
30
+ export const meta = {
31
+ name: "windsurf",
32
+ displayName: "Windsurf",
33
+ agentDir: ".windsurf",
34
+ configDir: (home) => path.join(home, ".windsurf"),
35
+ };
36
+
37
+ export async function install(installBase, accessKey, opts = {}) {
38
+ const { dryRun = false, env = "prod", scope = "project", home = installBase } = opts;
39
+ const base = meta.configDir(installBase);
40
+ const aidrDir = path.join(base, "coworker-ai");
41
+ const dlBase = downloadBase(env);
42
+
43
+ const hooksRes = await writeJsonMerge(
44
+ path.join(base, "hooks.json"),
45
+ buildWindsurfHooksJson(scope, home),
46
+ mergeWindsurfHooksJson,
47
+ { dryRun, mode: 0o644 },
48
+ );
49
+
50
+ const entries = [
51
+ [path.join(aidrDir, "env.sh"), T.agentEnvSh(accessKey, meta.agentDir), 0o755],
52
+ [path.join(aidrDir, "start_scanner.sh"), T.getStartScannerSh(dlBase, "windsurf"), 0o755],
53
+ [path.join(aidrDir, "ensure_scanner.sh"), T.WINDSURF_ENSURE_SCANNER_SH, 0o755],
54
+ [path.join(aidrDir, "pre_command.sh"), T.WINDSURF_PRE_COMMAND_SH, 0o755],
55
+ [path.join(aidrDir, "pre_read.sh"), T.WINDSURF_PRE_READ_SH, 0o755],
56
+ [path.join(aidrDir, "pre_write.sh"), T.WINDSURF_PRE_WRITE_SH, 0o755],
57
+ [path.join(aidrDir, "post_read.sh"), T.WINDSURF_POST_READ_SH, 0o755],
58
+ [path.join(aidrDir, "post_write.sh"), T.WINDSURF_POST_WRITE_SH, 0o755],
59
+ ];
60
+ const scriptResults = await Promise.all(
61
+ entries.map(([p, c, mode]) => writeFileSafe(p, c, { dryRun, force: true, backup: false, mode })),
62
+ );
63
+ const backupPaths = [
64
+ ...(hooksRes && hooksRes.backupPath ? [hooksRes.backupPath] : []),
65
+ ...scriptResults.map((r) => r && r.backupPath).filter(Boolean),
66
+ ];
67
+ return {
68
+ installed: [path.join(base, "hooks.json"), ...entries.map(([p]) => p)],
69
+ backupPaths,
70
+ };
71
+ }
72
+
73
+ export async function uninstall(installBase, opts = {}) {
74
+ const { dryRun = false } = opts;
75
+ const base = meta.configDir(installBase);
76
+ const aidrDir = path.join(base, "coworker-ai");
77
+ const binDir = path.join(base, "bin");
78
+
79
+ const candidateFiles = [
80
+ ...SCRIPT_FILES.map((n) => path.join(aidrDir, n)),
81
+ ...BIN_FILES.map((n) => path.join(binDir, n)),
82
+ ];
83
+ const checks = await Promise.all(
84
+ candidateFiles.map(async (p) => ((await pathExists(p)) ? p : null)),
85
+ );
86
+ const removedFiles = checks.filter((p) => p !== null);
87
+
88
+ const hooksAction = await unmergeJsonFile(
89
+ path.join(base, "hooks.json"),
90
+ unmergeWindsurfHooksJson,
91
+ { dryRun },
92
+ );
93
+
94
+ if (dryRun) {
95
+ return { removedFiles, settings: { action: "unmerge", hooksJson: hooksAction }, dirsRemoved: [] };
96
+ }
97
+
98
+ await Promise.all(removedFiles.map((f) => fsp.rm(f, { force: true })));
99
+ const dirChecks = await Promise.all(
100
+ [aidrDir, binDir].map(async (d) => ((await rmdirIfEmpty(d)) ? d : null)),
101
+ );
102
+ const dirsRemoved = dirChecks.filter((d) => d !== null);
103
+
104
+ return { removedFiles, settings: { action: "unmerge", hooksJson: hooksAction }, dirsRemoved };
105
+ }
@@ -0,0 +1,157 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { downloadBase } from "./verify.mjs";
8
+
9
+ const execFileP = promisify(execFile);
10
+
11
+ export function detectPlatform() {
12
+ const os = process.platform;
13
+ const arch = process.arch;
14
+ if (os === "linux" && arch === "x64") return "linux-x86_64";
15
+ if (os === "linux" && arch === "arm64") return "linux-aarch64";
16
+ if (os === "darwin" && arch === "arm64") return "darwin-aarch64";
17
+ if (os === "darwin" && arch === "x64") {
18
+ throw new Error("Intel Mac (darwin-x86_64) is not supported. Apple Silicon only.");
19
+ }
20
+ throw new Error(`Unsupported platform: ${os}/${arch}`);
21
+ }
22
+
23
+ /**
24
+ * Map Node.js platform/arch to the Opengrep release platform tag. Opengrep
25
+ * ships glibc (manylinux) and musl (musllinux) builds for Linux; Node cannot
26
+ * reliably detect libc, so we default to manylinux which covers the overwhelming
27
+ * majority of user environments (Debian/Ubuntu/Fedora/macOS Linux VMs).
28
+ * Users on Alpine can override with AI_SCANNER_OPENGREP.
29
+ */
30
+ export function detectOpengrepPlatform() {
31
+ const os = process.platform;
32
+ const arch = process.arch;
33
+ if (os === "darwin" && arch === "arm64") return "osx_arm64";
34
+ if (os === "darwin" && arch === "x64") return "osx_x86";
35
+ if (os === "linux" && arch === "x64") return "manylinux_x86";
36
+ if (os === "linux" && arch === "arm64") return "manylinux_aarch64";
37
+ throw new Error(`Unsupported platform for Opengrep: ${os}/${arch}`);
38
+ }
39
+
40
+ async function fetchBuffer(url, accessKey) {
41
+ const headers = accessKey ? { "x-access-key": accessKey } : {};
42
+ const res = await fetch(url, { redirect: "follow", headers });
43
+ if (!res.ok) throw new Error(`GET ${url} -> HTTP ${res.status}`);
44
+ const ab = await res.arrayBuffer();
45
+ return Buffer.from(ab);
46
+ }
47
+
48
+ async function fetchText(url, accessKey) {
49
+ const headers = accessKey ? { "x-access-key": accessKey } : {};
50
+ const res = await fetch(url, { redirect: "follow", headers });
51
+ if (!res.ok) return null;
52
+ return (await res.text()).trim();
53
+ }
54
+
55
+ async function clearQuarantine(destPath) {
56
+ if (process.platform !== "darwin") return;
57
+ try {
58
+ await execFileP("xattr", ["-d", "com.apple.quarantine", destPath]);
59
+ } catch {
60
+ // ignore (xattr may not be present or attribute may not be set)
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Download an asset from `binUrl` into `destPath`, validating SHA256 when a
66
+ * sibling `.sha256` file is available. Sets executable mode and strips macOS
67
+ * quarantine. Returns { path, sha256, verified }.
68
+ */
69
+ export async function fetchAsset(binUrl, destPath, { accessKey } = {}) {
70
+ const shaUrl = `${binUrl}.sha256`;
71
+ await fsp.mkdir(path.dirname(destPath), { recursive: true });
72
+
73
+ const buf = await fetchBuffer(binUrl, accessKey);
74
+ const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
75
+
76
+ let verified = false;
77
+ const expected = await fetchText(shaUrl, accessKey).catch(() => null);
78
+ if (expected) {
79
+ const want = expected.split(/\s+/)[0].toLowerCase();
80
+ if (want !== sha256) {
81
+ throw new Error(`SHA256 mismatch for ${binUrl}: got ${sha256}, expected ${want}`);
82
+ }
83
+ verified = true;
84
+ } else {
85
+ console.warn(`warning: ${shaUrl} unavailable; skipping SHA256 verification`);
86
+ }
87
+
88
+ const tmp = `${destPath}.tmp-${process.pid}`;
89
+ await fsp.writeFile(tmp, buf);
90
+ await fsp.chmod(tmp, 0o755);
91
+ await fsp.rename(tmp, destPath);
92
+
93
+ await clearQuarantine(destPath);
94
+
95
+ return { path: destPath, sha256, verified };
96
+ }
97
+
98
+ /**
99
+ * Download ai-scanner for the current platform into `destPath`.
100
+ * Returns { path, platform, sha256, verified }.
101
+ */
102
+ export async function fetchBinary(destPath, { env = "prod", accessKey, version } = {}) {
103
+ const platform = detectPlatform();
104
+ const base = downloadBase(env);
105
+ const pathPrefix = version ? `v${version}/` : "";
106
+ const binUrl = `${base}/${pathPrefix}${platform}`;
107
+
108
+ const result = await fetchAsset(binUrl, destPath, { accessKey });
109
+ return { ...result, platform };
110
+ }
111
+
112
+ // Pinned Opengrep release used by aidr. Must be updated in lockstep with
113
+ // scripts/.opengrep-version, LICENSES/opengrep-LGPL-2.1.txt, and the Dockerfile
114
+ // ARGs. Fetched directly from GitHub Releases (public, LGPL-2.1) rather than
115
+ // proxying via the licensed download endpoint: aidr's role here is to
116
+ // place the upstream binary beside ai-scanner, not to gate access to it.
117
+ const OPENGREP_VERSION = "v1.19.0";
118
+ const OPENGREP_SHA256 = {
119
+ osx_arm64: "8e9f2ecfd2b6f0c824aa111b9663f93b1d5fd2e59d5e5a25b5d6f84f922087ae",
120
+ osx_x86: "6159af8070d28c0e2df642a68b3be4ce512442b5b31fae85fb3218dac5301f9d",
121
+ manylinux_x86: "1d69a41beb88e8e7917f26cc6a16c1edf298f31402807e6d1afbb5d8684c3590",
122
+ manylinux_aarch64: "7141748e929292e2b672f12515035a01643705010f28970c66ae43612162213e",
123
+ musllinux_x86: "02cf0b1be28d4bafd21fc836d59f4bdccf3ff0635dc3ec3c1a33560ab76a65e5",
124
+ musllinux_aarch64: "cbe97968569491bbaea33cf88384177cf602319550d10da066297c2421c24466",
125
+ };
126
+
127
+ /**
128
+ * Download the bundled Opengrep binary (LGPL-2.1) for the current platform
129
+ * into `destPath`. Opengrep is invoked by ai-scanner as a separate process;
130
+ * shipping it as a standalone file (not baked into ai-scanner) preserves the
131
+ * LGPL replace-ability requirement. Override the resolved path at runtime
132
+ * with the `AI_SCANNER_OPENGREP` environment variable.
133
+ *
134
+ * Returns { path, platform, sha256, verified }.
135
+ */
136
+ export async function fetchOpengrep(destPath) {
137
+ const platform = detectOpengrepPlatform();
138
+ const expectedSha = OPENGREP_SHA256[platform];
139
+ if (!expectedSha) throw new Error(`no pinned SHA256 for Opengrep platform ${platform}`);
140
+
141
+ const binUrl = `https://github.com/opengrep/opengrep/releases/download/${OPENGREP_VERSION}/opengrep_${platform}`;
142
+
143
+ await fsp.mkdir(path.dirname(destPath), { recursive: true });
144
+ const buf = await fetchBuffer(binUrl);
145
+ const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
146
+ if (sha256 !== expectedSha) {
147
+ throw new Error(`SHA256 mismatch for opengrep_${platform}: got ${sha256}, expected ${expectedSha}`);
148
+ }
149
+
150
+ const tmp = `${destPath}.tmp-${process.pid}`;
151
+ await fsp.writeFile(tmp, buf);
152
+ await fsp.chmod(tmp, 0o755);
153
+ await fsp.rename(tmp, destPath);
154
+ await clearQuarantine(destPath);
155
+
156
+ return { path: destPath, platform, sha256, verified: true };
157
+ }
@@ -0,0 +1,83 @@
1
+ import { writeFileSync, mkdirSync, readFileSync, chmodSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ // Extension ID will be determined after Chrome Web Store publication.
5
+ // For now use a placeholder that operators can override via AIDR_EXTENSION_ID env var.
6
+ const DEFAULT_EXTENSION_ID = "AIDR_EXTENSION_ID_PLACEHOLDER";
7
+
8
+ export async function installBrowserExtension(homeDir, binaryPath) {
9
+ const extensionId = process.env.AIDR_EXTENSION_ID || DEFAULT_EXTENSION_ID;
10
+ const platform = process.platform;
11
+
12
+ // 1. Write Native Messaging Host manifests for Chrome/Edge/Brave/Chromium
13
+
14
+ // Generate a wrapper script so the binary is invoked in native-messaging-host mode
15
+ const binDir = join(homeDir, ".claude", "bin");
16
+ mkdirSync(binDir, { recursive: true });
17
+ const wrapperPath = join(binDir, "ai-scanner-nativehost.sh");
18
+ const wrapperContent = `#!/bin/sh\nexec "${binaryPath}" native-messaging-host "$@"\n`;
19
+ try {
20
+ writeFileSync(wrapperPath, wrapperContent, { mode: 0o755 });
21
+ } catch (_e) {
22
+ // fall back to binary path directly
23
+ }
24
+
25
+ const nativeManifest = {
26
+ name: "jp.coworker.aidr.browser",
27
+ description: "AIDR Shadow IT Monitor bridge",
28
+ path: wrapperPath,
29
+ type: "stdio",
30
+ allowed_origins: [`chrome-extension://${extensionId}/`],
31
+ };
32
+
33
+ const chromePaths = getNativeMessagingPaths(platform, homeDir);
34
+ for (const dir of chromePaths) {
35
+ try {
36
+ mkdirSync(dir, { recursive: true });
37
+ writeFileSync(
38
+ join(dir, "jp.coworker.aidr.browser.json"),
39
+ JSON.stringify(nativeManifest, null, 2)
40
+ );
41
+ } catch (_e) {
42
+ // fail-open: skip paths we can't write
43
+ }
44
+ }
45
+
46
+ // 2. Record in ~/.claude/ai-scanner-config.json
47
+ const configPath = join(homeDir, ".claude", "ai-scanner-config.json");
48
+ let config = {};
49
+ try {
50
+ config = JSON.parse(readFileSync(configPath, "utf8"));
51
+ } catch (_e) {
52
+ // file doesn't exist yet or is unreadable; start fresh
53
+ }
54
+ config.browser_extension = true;
55
+ config.extension_id = extensionId;
56
+ try {
57
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
58
+ } catch (_e) {
59
+ // fail-open: config write is best-effort
60
+ }
61
+
62
+ console.log("✓ Browser extension native messaging host installed");
63
+ console.log(` Extension ID: ${extensionId}`);
64
+ console.log(" Open Chrome and install the AIDR Shadow IT Monitor extension to complete setup.");
65
+ }
66
+
67
+ function getNativeMessagingPaths(platform, homeDir) {
68
+ if (platform === "darwin") {
69
+ return [
70
+ join(homeDir, "Library/Application Support/Google/Chrome/NativeMessagingHosts"),
71
+ join(homeDir, "Library/Application Support/Microsoft Edge/NativeMessagingHosts"),
72
+ join(homeDir, "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"),
73
+ join(homeDir, "Library/Application Support/Chromium/NativeMessagingHosts"),
74
+ ];
75
+ } else {
76
+ return [
77
+ join(homeDir, ".config/google-chrome/NativeMessagingHosts"),
78
+ join(homeDir, ".config/microsoft-edge/NativeMessagingHosts"),
79
+ join(homeDir, ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts"),
80
+ join(homeDir, ".config/chromium/NativeMessagingHosts"),
81
+ ];
82
+ }
83
+ }