Files
rohitg00__agentmemory/plugin/scripts/post-tool-use.mjs
Ross Story a0da02b6b3 Add GitHub Copilot CLI support (#534)
* feat: add Copilot CLI plugin asset slice

- plugin/.plugin/plugin.json: Copilot manifest with name/version/skills/mcpServers/hooks refs
- plugin/.mcp.copilot.json: MCP server config with type:local, npx, env passthrough, tools:[*]
- plugin/hooks/hooks.copilot.json: Copilot hooks (version:1) with 11 supported events and PreToolUse matcher
- test/copilot-plugin.test.ts: 11 tests covering manifest, MCP config, and hooks validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Copilot CLI connect support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add GitHub Copilot CLI support

Adds Copilot CLI support through a root plugin manifest, Copilot-specific MCP and hook configuration, and a connect adapter for MCP-only setup.

Includes Windows-safe Copilot MCP command generation, COPILOT_HOME handling, Copilot hook payload normalization, generated hook scripts, and targeted tests for plugin shape, hook execution, and connect behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden Copilot hook handling

Addresses upstream AI review suggestions by aligning the Copilot preToolUse matcher with the hook allowlist, narrowing hook payload fields at runtime, normalizing subagent fallbacks, and tightening hook config validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Copilot to first-run onboarding

Includes GitHub Copilot CLI in the first-run agent picker and adds a regression test so the Copilot setup path remains discoverable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Default onboarding to Copilot inside Copilot CLI

Detect Copilot CLI environment markers during first-run setup so pressing Enter wires the current agent instead of the historical Claude Code default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support framed stdio MCP transport

Accept Content-Length framed JSON-RPC messages in addition to the existing newline-delimited transport so Copilot CLI can initialize the standalone MCP server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Narrow Copilot pre-tool session ids

Ensures pre-tool-use only forwards string session IDs and falls back to unknown for invalid Copilot payload values, with regression coverage for the generated plugin script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ross Story <rostory@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-28 09:54:54 +01:00

122 lines
3.5 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/post-tool-use.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
const toolName = data.tool_name ?? data.toolName;
const toolInput = data.tool_input ?? data.toolArgs;
const { imageData, cleanOutput } = extractImageData(toolOutput(data));
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "post_tool_use",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
tool_name: toolName,
tool_input: toolInput,
tool_output: truncate(cleanOutput, 8e3),
...imageData ? { image_data: imageData } : {}
}
}),
signal: AbortSignal.timeout(3e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
function toolOutput(data) {
if (data.tool_response !== void 0) return data.tool_response;
if (data.tool_output !== void 0) return data.tool_output;
const result = data.tool_result ?? data.toolResult;
if (typeof result === "object" && result !== null) {
const obj = result;
return obj.text_result_for_llm ?? obj.textResultForLlm ?? result;
}
return result;
}
function isBase64Image(val) {
return typeof val === "string" && (val.startsWith("data:image/") || val.startsWith("iVBORw0KGgo") || val.startsWith("/9j/"));
}
function extractImageData(output) {
if (isBase64Image(output)) return {
imageData: output,
cleanOutput: "[image data extracted]"
};
if (typeof output === "object" && output !== null && !Array.isArray(output)) {
const obj = output;
let imageData;
const clean = {};
for (const [key, val] of Object.entries(obj)) if (!imageData && isBase64Image(val)) {
imageData = val;
clean[key] = "[image data extracted]";
} else clean[key] = val;
return {
imageData,
cleanOutput: clean
};
}
return {
imageData: void 0,
cleanOutput: output
};
}
function truncate(value, max) {
if (typeof value === "string" && value.length > max) return value.slice(0, max) + "\n[...truncated]";
if (typeof value === "object" && value !== null) {
const str = JSON.stringify(value);
if (str.length > max) return str.slice(0, max) + "...[truncated]";
return value;
}
return value;
}
main();
//#endregion
export { };
//# sourceMappingURL=post-tool-use.mjs.map