Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
"scripts": {
"build": "tsdown && (cp iii-config.yaml dist/ 2>/dev/null || true) && (cp iii-config.docker.yaml dist/ 2>/dev/null || true) && (cp docker-compose.yml dist/ 2>/dev/null || true) && (cp .env.example dist/ 2>/dev/null || true) && mkdir -p dist/viewer && cp src/viewer/index.html dist/viewer/ && cp src/viewer/favicon.svg dist/viewer/",
"dev": "tsx src/index.ts",
"start": "node dist/cli.mjs",
"migrate": "node dist/functions/migrate.js",
"start": "tsx dist/cli.mjs",
"migrate": "tsx dist/functions/migrate.js",
"test": "vitest run --exclude test/integration.test.ts",
"test:watch": "vitest --exclude test/integration.test.ts",
"test:integration": "vitest run test/integration.test.ts",
"test:all": "vitest run",
"skills:gen": "tsx scripts/skills/generate.ts",
"skills:check": "tsx scripts/skills/generate.ts --check && tsx scripts/skills/check.ts",
"bench:load": "node --import tsx benchmark/load-100k.ts",
"bench:load": "tsx benchmark/load-100k.ts",
"eval:longmemeval": "tsx eval/runner/longmemeval.ts",
"eval:coding-life": "tsx eval/runner/coding-life.ts"
},
Expand Down Expand Up @@ -87,6 +87,7 @@
"protobufjs": "^7.5.8"
},
"engines": {
"node": ">=20.0.0"
"node": ">=20.0.0",
"bun": ">=1.1.0"
}
}
22 changes: 11 additions & 11 deletions plugin/hooks/hooks.copilot.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,68 @@
"sessionStart": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/session-start.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/session-start.mjs\""
}
],
"userPromptSubmitted": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/prompt-submit.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/prompt-submit.mjs\""
}
],
"preToolUse": [
{
"type": "command",
"matcher": "edit|write|create|read|view|glob|grep",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/pre-tool-use.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
}
],
"postToolUse": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/post-tool-use.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
}
],
"postToolUseFailure": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/post-tool-failure.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/post-tool-failure.mjs\""
}
],
"preCompact": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/pre-compact.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
}
],
"agentStop": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/stop.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/stop.mjs\""
}
],
"sessionEnd": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/session-end.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/session-end.mjs\""
}
],
"subagentStart": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/subagent-start.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/subagent-start.mjs\""
}
],
"subagentStop": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/subagent-stop.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/subagent-stop.mjs\""
}
],
"notification": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/notification.mjs"
"command": "node \"${COPILOT_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${COPILOT_PLUGIN_ROOT}/scripts/notification.mjs\""
}
]
}
Expand Down
24 changes: 12 additions & 12 deletions plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\""
}
]
}
Expand All @@ -15,7 +15,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\""
}
]
}
Expand All @@ -26,7 +26,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
}
]
}
Expand All @@ -36,7 +36,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
}
]
}
Expand All @@ -46,7 +46,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-failure.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-failure.mjs\""
}
]
}
Expand All @@ -56,7 +56,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
}
]
}
Expand All @@ -66,7 +66,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-start.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-start.mjs\""
}
]
}
Expand All @@ -76,7 +76,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-stop.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-stop.mjs\""
}
]
}
Expand All @@ -86,7 +86,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/notification.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/notification.mjs\""
}
]
}
Expand All @@ -96,7 +96,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/task-completed.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/task-completed.mjs\""
}
]
}
Expand All @@ -106,7 +106,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\""
}
]
}
Expand All @@ -116,7 +116,7 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs\""
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs\""
}
]
}
Expand Down
44 changes: 44 additions & 0 deletions scripts/hook-runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env node
/**
* Cross-runtime hook script launcher.
*
* Detects whether the current runtime is Bun or Node and spawns the target
* hook script with the appropriate runtime. This lets hook.json configs
* use a single static "command" entry while respecting whichever runtime
* the user has installed.
*
* Usage (from hooks.json):
* "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.mjs\" \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\""
*
* The first argument is the hook script to run. All subsequent arguments
* are forwarded to the hook script.
*/
import { spawn } from "node:child_process";
import process from "node:process";

const IS_BUN = typeof process.versions.bun === "string";
const runtime = IS_BUN ? "bun" : "node";
const hookScript = process.argv[2];

if (!hookScript) {
process.stderr.write("hook-runner: missing script argument\n");
process.exit(1);
}

// Forward remaining arguments (hook engine passes stdin JSON)
const childArgs = [hookScript, ...process.argv.slice(3)];

const child = spawn(runtime, childArgs, {
stdio: "inherit",
shell: IS_BUN && process.platform === "win32",
env: process.env,
});

child.on("exit", (code) => {
process.exit(code ?? 1);
});

child.on("error", (err) => {
process.stderr.write(`hook-runner: failed to spawn ${runtime}: ${err.message}\n`);
process.exit(1);
});
25 changes: 24 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const CORE_TOOLS_COUNT = getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name)
const __dirname = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
const IS_WINDOWS = platform() === "win32";
const IS_BUN = typeof process.versions.bun === "string";
const IS_VERBOSE =
args.includes("--verbose") ||
args.includes("-v") ||
Expand Down Expand Up @@ -386,6 +387,23 @@ function findIiiConfig(): string {
return "";
}

function writeBunCompatibleConfig(originalPath: string): string {
const bunPath = join(homedir(), ".agentmemory", "iii-config.bun.yaml");
mkdirSync(join(homedir(), ".agentmemory"), { recursive: true });
const runtimeCmd = IS_WINDOWS ? "bun.exe run" : "bun run";
const original = readFileSync(originalPath, "utf-8");
const needle = "- node dist/index.mjs";
if (!original.includes(needle)) {
throw new Error(
`Bun config rewrite failed: expected "${needle}" not found in ${originalPath}`,
);
}
const content = original.replaceAll(needle, `- ${runtimeCmd} src/index.ts`);
writeFileSync(bunPath, content, "utf-8");
vlog(`wrote Bun-compatible config: ${bunPath}`);
return bunPath;
}

function whichBinary(name: string): string | null {
const cmd = IS_WINDOWS ? "where" : "which";
try {
Expand Down Expand Up @@ -450,6 +468,7 @@ function iiiBinVersion(binPath: string): string | null {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 3000,
...(IS_WINDOWS && IS_BUN ? { shell: true } : {}),
});
const match = out.match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
return match ? match[1]! : null;
Expand Down Expand Up @@ -844,6 +863,7 @@ function spawnEngineBackground(
detached: true,
stdio: ["ignore", "ignore", "pipe"],
windowsHide: true,
...(IS_WINDOWS && IS_BUN ? { shell: true } : {}),
});
const isDocker = label.includes("Docker");
if (!isDocker && typeof child.pid === "number") {
Expand Down Expand Up @@ -908,7 +928,10 @@ function pickCompatibleIii(candidates: Array<string | null | undefined>): string
}

async function startEngine(): Promise<boolean> {
const configPath = findIiiConfig();
const originalConfigPath = findIiiConfig();
const configPath = (IS_BUN && originalConfigPath)
? writeBunCompatibleConfig(originalConfigPath)
: originalConfigPath;
const pathIii = whichBinary("iii");
vlog(`iii binary: ${pathIii ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`);

Expand Down
21 changes: 18 additions & 3 deletions src/functions/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,24 @@ import type {
GraphNode,
GraphEdge,
} from "../types.js";
import { lookup } from "node:dns/promises";
import { isIP } from "node:net";

const IS_BUN = typeof (globalThis as any).Bun?.dns?.lookup === "function";

async function dnsLookup(
hostname: string,
options?: { all?: boolean },
): Promise<{ address: string; family: 4 | 6 }[]> {
if (IS_BUN) {
const results = await (globalThis as any).Bun.dns.lookup(hostname, options);
return results.map((r: { address: string; family: 4 | 6 }) => r);
}
const { lookup } = await import("node:dns/promises");
return lookup(hostname, { ...options, all: true }) as Promise<
{ address: string; family: 4 | 6 }[]
>;
}

function isPrivateIP(ip: string): boolean {
if (ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0") return true;
if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true;
Expand All @@ -41,8 +56,8 @@ async function isAllowedUrl(urlStr: string): Promise<boolean> {

if (!isIP(host)) {
try {
const resolved = await lookup(host, { all: true });
if (resolved.some((r) => isPrivateIP(r.address))) return false;
const resolved = await dnsLookup(host, { all: true });
if (resolved.some((r: { address: string }) => isPrivateIP(r.address))) return false;
} catch {
// DNS resolution failed — allow the URL (the actual fetch will fail if unreachable)
}
Expand Down
Loading