@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.
- package/README.md +55 -0
- package/bin/aidr.js +2 -0
- package/package.json +29 -0
- package/src/agents/_stub.mjs +18 -0
- package/src/agents/aider.mjs +5 -0
- package/src/agents/amazonq.mjs +5 -0
- package/src/agents/amp.mjs +5 -0
- package/src/agents/antigravity.mjs +5 -0
- package/src/agents/claude.mjs +92 -0
- package/src/agents/cline.mjs +5 -0
- package/src/agents/codex.mjs +225 -0
- package/src/agents/continue.mjs +5 -0
- package/src/agents/copilot.mjs +5 -0
- package/src/agents/crush.mjs +5 -0
- package/src/agents/cursor.mjs +109 -0
- package/src/agents/gemini.mjs +66 -0
- package/src/agents/index.mjs +36 -0
- package/src/agents/jetbrains.mjs +5 -0
- package/src/agents/kiro.mjs +79 -0
- package/src/agents/opencode.mjs +5 -0
- package/src/agents/qwen.mjs +5 -0
- package/src/agents/roo.mjs +5 -0
- package/src/agents/standalone.mjs +43 -0
- package/src/agents/trae.mjs +5 -0
- package/src/agents/windsurf.mjs +105 -0
- package/src/binary-fetcher.mjs +157 -0
- package/src/browser-extension.mjs +83 -0
- package/src/cli.mjs +494 -0
- package/src/detect.mjs +39 -0
- package/src/fs-utils.mjs +247 -0
- package/src/merge.mjs +412 -0
- package/src/scheduled.mjs +219 -0
- package/src/templates.mjs +759 -0
- package/src/toml-merge.mjs +167 -0
- package/src/verify.mjs +51 -0
|
@@ -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,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,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,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
|
+
}
|