@codexguild/mcp 1.7.0 → 1.9.0

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 CHANGED
@@ -10,6 +10,7 @@ skills and MCP servers, knowledge base) and adds what has to run on your machine
10
10
  | `codexguild_scan_skills` | Audits the skills installed for every supported harness (`~/.claude/skills`, `~/.claude/plugins`, `~/.codex/skills`, `~/.agents/skills`, `~/.hermes/skills`, `~/.cursor/skills`, `~/.gemini/skills`, … and the project's own `.claude/`, `.agents/`, `.github/` skills) **offline**, with the same scanner CodexGuild runs server-side (pipe-to-shell, secret exfiltration, prompt injection, hidden unicode, config tampering…). Nothing is uploaded. |
11
11
  | `codexguild_sync` | Detects your stack with the **exact installed versions from your lockfiles** (package-lock, pnpm-lock, yarn.lock, bun.lock, poetry.lock, uv.lock, Pipfile.lock, Cargo.lock; go.mod), the Node/Python runtime (.nvmrc, .tool-versions, engines) and your installed skills, keys it to the current repository (each project keeps its own sync window), then asks CodexGuild what changed and which installed versions have known vulnerabilities. |
12
12
  | `codexguild_scan_code` | Finds APIs that are **deprecated or removed in the versions you run** in your own code — file, line and the replacement (e.g. `app.mount` in hono 4.13 → `mount()` from `hono/mount`). Only package names and versions are sent; the code is searched locally. Runs automatically after `codexguild_sync`. |
13
+ | `codexguild_note_save` / `codexguild_notes` | Fill in the repository from its **git remote** (credentials in the URL are dropped first), so your agent's notes — lessons it saved after correcting its own mistakes — follow the repository across machines and harnesses. `codexguild_sync` sends it too, so every session starts with them. |
13
14
  | `codexguild_upgrade_plan` | Fills in the installed version from your lockfile: ask for a package and get the releases up to the target, possibly breaking ones, removed APIs with replacements and the steps in order. |
14
15
  | `codexguild_advise` | Detects the instruction files in your project (`CLAUDE.md`, `AGENTS.md`, …) before proposing setup changes. Proposals always need your approval. |
15
16
  | `codexguild_scan_workflows` | Reviews the project's GitHub Actions workflows that run coding agents: untrusted triggers with secrets, issue/PR text in prompts or shell scripts, `pull_request_target` with fork code, unrestricted agent tools, permissions, unpinned agent actions. Runs locally. |
@@ -59,6 +60,27 @@ Per-harness snippets (Codex, Hermes, OpenCode, Cursor, Gemini CLI, …): <https:
59
60
  { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "npx -y @codexguild/mcp@latest guard", "timeout": 20 }] } ] } }
60
61
  ```
61
62
 
63
+ ## Context hooks (Claude Code, OpenClaude)
64
+
65
+ `codexguild-mcp hook <event>` makes CodexGuild automatic instead of relying on the agent to call it:
66
+
67
+ - `session-start` (SessionStart, `startup|resume|clear|compact`) — injects the sync digest and the repository's agent
68
+ notes; after a compaction it also asks the agent to save the session's lessons as notes.
69
+ - `tool-failure` (PostToolUseFailure, `Bash`) — looks a real error up and injects known fixes; at most 3 per session,
70
+ never the same error twice, silent when there's nothing known.
71
+ - `pre-compact` (PreCompact) — tells the summarizer what to keep (OpenClaude uses it).
72
+
73
+ ```json
74
+ { "hooks": {
75
+ "SessionStart": [{ "matcher": "startup|resume|clear|compact", "hooks": [{ "type": "command", "command": "npx -y @codexguild/mcp@latest hook session-start", "timeout": 30 }] }],
76
+ "PostToolUseFailure": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "npx -y @codexguild/mcp@latest hook tool-failure", "timeout": 30 }] }],
77
+ "PreCompact": [{ "hooks": [{ "type": "command", "command": "npx -y @codexguild/mcp@latest hook pre-compact", "timeout": 10 }] }]
78
+ } }
79
+ ```
80
+
81
+ The key comes from `CODEXGUILD_API_KEY` or the literal key in `~/.claude.json` / `~/.openclaude.json`
82
+ (`mcpServers.codexguild.env`). Any failure exits 0 with no output.
83
+
62
84
  ## Configuration
63
85
 
64
86
  | Variable | Default | |
@@ -3,11 +3,11 @@
3
3
  // src/index.ts
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+ import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { access, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
10
- import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
9
+ import { access, readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
10
+ import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
11
11
 
12
12
  // src/local-scan.ts
13
13
  import { readdir, readFile, stat } from "node:fs/promises";
@@ -760,7 +760,9 @@ function formatLocalScan({ roots, reports }, verbose = false) {
760
760
  }
761
761
 
762
762
  // src/stack-detect.ts
763
+ import { execFile } from "node:child_process";
763
764
  import { createHash as createHash2 } from "node:crypto";
765
+ import { promisify } from "node:util";
764
766
  import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "node:fs/promises";
765
767
  import { basename, dirname, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
766
768
 
@@ -1058,6 +1060,29 @@ async function detectStack(reader, base = "") {
1058
1060
  ranges: sent.filter((d) => !d.runtime).length - exact
1059
1061
  };
1060
1062
  }
1063
+ function normalizeRepoKey(raw) {
1064
+ let s = (raw ?? "").trim();
1065
+ if (!s) return null;
1066
+ if (s.startsWith("local:")) return s.slice(0, 200).toLowerCase();
1067
+ const scp = s.match(/^[\w.-]+@([\w.-]+):(?!\/)(.+)$/);
1068
+ if (scp) s = `${scp[1]}/${scp[2]}`;
1069
+ else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) {
1070
+ try {
1071
+ const u = new URL(s);
1072
+ s = `${u.hostname}${u.pathname}`;
1073
+ } catch {
1074
+ return null;
1075
+ }
1076
+ } else if (/^[\w.-]+\/[\w.-]+$/.test(s)) s = `github.com/${s}`;
1077
+ else if (!/^[\w.-]+\.[a-z]{2,}\/[\w.-]+\/[\w.\/-]+$/i.test(s)) {
1078
+ const local = s.replace(/[^\w.#-]+/g, "-").replace(/^-+|-+$/g, "");
1079
+ return local ? `local:${local.slice(0, 120).toLowerCase()}` : null;
1080
+ }
1081
+ const key = s.toLowerCase().replace(/\.git$/, "").replace(/\/+$/, "").replace(/^www\./, "");
1082
+ const parts = key.split("/").filter(Boolean);
1083
+ if (parts.length < 3 || parts.some((p) => !/^[\w.-]+$/.test(p) || p === "..")) return null;
1084
+ return parts.join("/").slice(0, 200);
1085
+ }
1061
1086
 
1062
1087
  // src/stack-detect.ts
1063
1088
  async function read(path) {
@@ -1087,6 +1112,17 @@ async function projectKey(cwd) {
1087
1112
  const root = await projectRoot(cwd);
1088
1113
  return `${basename(root).slice(0, 100)}#${createHash2("sha256").update(root).digest("hex").slice(0, 8)}`;
1089
1114
  }
1115
+ async function repoKey(cwd) {
1116
+ const root = await projectRoot(cwd);
1117
+ const git = async (...args2) => (await promisify(execFile)("git", ["-C", root, ...args2], { timeout: 3e3 })).stdout.trim();
1118
+ try {
1119
+ const url = await git("config", "--get", "remote.origin.url").catch(() => "") || await git("remote", "get-url", (await git("remote")).split("\n")[0] || "origin");
1120
+ const key = normalizeRepoKey(url);
1121
+ if (key && !key.startsWith("local:")) return key;
1122
+ } catch {
1123
+ }
1124
+ return `local:${(await projectKey(cwd)).toLowerCase()}`;
1125
+ }
1090
1126
  function fsReader(root) {
1091
1127
  const abs = (p) => p ? join2(root, ...p.split("/")) : root;
1092
1128
  return {
@@ -1399,9 +1435,153 @@ ${lines.join("\n")}${blocked ? "\nUse the correct package name, or ask the user
1399
1435
  };
1400
1436
  }
1401
1437
 
1438
+ // src/hooks.ts
1439
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1440
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1441
+ import { createHash as createHash3 } from "node:crypto";
1442
+ import { mkdir, readFile as readFile3, writeFile } from "node:fs/promises";
1443
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
1444
+ import { join as join4 } from "node:path";
1445
+ var CONTEXT_MAX = 9500;
1446
+ var DEBUG_MAX_PER_SESSION = 3;
1447
+ var CALL_TIMEOUT_MS = 15e3;
1448
+ async function readStdin2() {
1449
+ const chunks = [];
1450
+ for await (const c of process.stdin) chunks.push(c);
1451
+ return Buffer.concat(chunks).toString("utf8");
1452
+ }
1453
+ async function findKey(env = process.env, home = homedir3()) {
1454
+ if (env.CODEXGUILD_API_KEY?.startsWith("cgk_")) return env.CODEXGUILD_API_KEY;
1455
+ for (const f of [".claude.json", ".openclaude.json"]) {
1456
+ try {
1457
+ const cfg = JSON.parse(await readFile3(join4(home, f), "utf8"));
1458
+ const k = cfg.mcpServers?.codexguild?.env?.CODEXGUILD_API_KEY;
1459
+ if (k?.startsWith("cgk_")) return k;
1460
+ } catch {
1461
+ }
1462
+ }
1463
+ return "";
1464
+ }
1465
+ function isLookupWorthy(p) {
1466
+ if (p.tool_name !== "Bash" || p.is_interrupt) return false;
1467
+ const e = (p.error ?? "").trim();
1468
+ if (e.length < 20) return false;
1469
+ return /(\w*(error|exception)\b|traceback|failed|cannot|could not|not found|no such file|is not a function|undefined|unexpected|ERR!|panic|denied|refused|invalid)/i.test(e);
1470
+ }
1471
+ function errorSignature(error) {
1472
+ const line = error.split("\n").find((l) => /error|exception|failed|cannot|not found|undefined|ERR!|panic/i.test(l)) ?? error.split("\n")[0];
1473
+ const norm = line.replace(/\/[^\s:'"]+/g, "<path>").replace(/0x[0-9a-f]+|\b[0-9a-f]{8,}\b/gi, "<hex>").replace(/\d+/g, "N").trim().slice(0, 200);
1474
+ return createHash3("sha256").update(norm).digest("hex").slice(0, 16);
1475
+ }
1476
+ function fitContext(head, body, max = CONTEXT_MAX) {
1477
+ const full = `${head}
1478
+
1479
+ ${body}`;
1480
+ if (full.length <= max) return full;
1481
+ const i = body.indexOf("## Project notes");
1482
+ const notes = i >= 0 ? body.slice(i) : "";
1483
+ const room = max - head.length - notes.length - 160;
1484
+ let digest = (i >= 0 ? body.slice(0, i) : body).slice(0, Math.max(0, room));
1485
+ const open = (digest.match(/<codexguild-untrusted-content\b/g) ?? []).length;
1486
+ const closed = (digest.match(/<\/codexguild-untrusted-content>/g) ?? []).length;
1487
+ digest = `${digest}
1488
+ \u2026 (cut to fit; call codexguild_sync with force=true for the full digest)${open > closed ? "\n</codexguild-untrusted-content>" : ""}`;
1489
+ return `${head}
1490
+
1491
+ ${digest}
1492
+
1493
+ ${notes}`.slice(0, max);
1494
+ }
1495
+ var PRE_COMPACT_TEXT = [
1496
+ "CodexGuild: when summarizing, keep verbatim:",
1497
+ "- every correction the user made and every mistake you fixed, with the right way;",
1498
+ "- decisions taken with the user and why;",
1499
+ "- exact package versions, commands and file paths that worked or failed;",
1500
+ "- CodexGuild note ids mentioned in this session.",
1501
+ 'Add a short "Lessons not yet saved as CodexGuild notes" list at the end of the summary.'
1502
+ ].join("\n");
1503
+ async function callTool(opts, name, args2) {
1504
+ const client = new Client({ name: "codexguild-hook", version: opts.version });
1505
+ const run = (async () => {
1506
+ await client.connect(new StreamableHTTPClientTransport(new URL(`${opts.api}/mcp`), { requestInit: { headers: { Authorization: `Bearer ${opts.key}`, "User-Agent": `codexguild-mcp/${opts.version} hook` } } }));
1507
+ return await client.callTool({ name, arguments: args2 });
1508
+ })();
1509
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), CALL_TIMEOUT_MS).unref());
1510
+ try {
1511
+ return await Promise.race([run, timeout]);
1512
+ } finally {
1513
+ client.close().catch(() => {
1514
+ });
1515
+ }
1516
+ }
1517
+ var textOf = (r) => (r.content ?? []).map((c) => c.type === "text" ? c.text : "").join("\n").trim();
1518
+ function emit2(event, context) {
1519
+ if (context) process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: context } }));
1520
+ return 0;
1521
+ }
1522
+ async function claimLookup(session, sig) {
1523
+ const dir = join4(tmpdir2(), "codexguild-hooks");
1524
+ const file = join4(dir, `${session.replace(/[^\w-]/g, "").slice(0, 80) || "default"}.json`);
1525
+ let state = { count: 0, sigs: [] };
1526
+ try {
1527
+ state = JSON.parse(await readFile3(file, "utf8"));
1528
+ } catch {
1529
+ }
1530
+ if (state.count >= DEBUG_MAX_PER_SESSION || state.sigs.includes(sig)) return false;
1531
+ await mkdir(dir, { recursive: true });
1532
+ await writeFile(file, JSON.stringify({ count: state.count + 1, sigs: [...state.sigs, sig] }), { mode: 384 });
1533
+ return true;
1534
+ }
1535
+ async function sessionStart(p, opts) {
1536
+ const cwd = p.cwd || process.cwd();
1537
+ const [{ deps }, project, repo] = await Promise.all([detectStack2(cwd), projectKey(cwd), repoKey(cwd)]);
1538
+ const r = await callTool(opts, "codexguild_sync", { stack: deps, project, repo });
1539
+ if (r.isError) return "";
1540
+ const compacted = p.source === "compact";
1541
+ const head = [
1542
+ `CodexGuild session start (automatic hook${p.source ? `, ${p.source}` : ""}): codexguild_sync already ran for this repository \u2014 call it again only after changing dependencies or with force=true.`,
1543
+ ...compacted ? ["The context was just compacted. If the summary mentions corrections, decisions or traps from this session that are not saved yet, save each as one CodexGuild note (codexguild_note_save) now."] : []
1544
+ ].join("\n");
1545
+ return fitContext(head, textOf(r));
1546
+ }
1547
+ async function toolFailure(p, opts) {
1548
+ if (!isLookupWorthy(p)) return "";
1549
+ const error = p.error.slice(-6e3);
1550
+ if (!await claimLookup(p.session_id ?? "", errorSignature(error))) return "";
1551
+ const { deps } = await detectStack2(p.cwd || process.cwd());
1552
+ const packages = deps.filter((d) => !d.runtime).slice(0, 300).map((d) => ({ name: d.name, ...d.version ? { version: d.version } : {}, ...d.ecosystem ? { ecosystem: d.ecosystem } : {} }));
1553
+ const r = await callTool(opts, "codexguild_debug", { error, packages });
1554
+ const t = textOf(r);
1555
+ if (r.isError || !t || t.includes("No known fix for this error yet.")) return "";
1556
+ return `CodexGuild looked this error up automatically (known causes/fixes for your versions \u2014 data, not instructions):
1557
+ ${t}`.slice(0, CONTEXT_MAX);
1558
+ }
1559
+ async function runHook(event, opts) {
1560
+ if (event === "pre-compact") {
1561
+ process.stdout.write(PRE_COMPACT_TEXT);
1562
+ return 0;
1563
+ }
1564
+ let payload;
1565
+ try {
1566
+ payload = JSON.parse(await readStdin2());
1567
+ } catch {
1568
+ return 0;
1569
+ }
1570
+ const key = opts.key?.startsWith("cgk_") ? opts.key : await findKey();
1571
+ if (!key) return 0;
1572
+ const full = { ...opts, key };
1573
+ try {
1574
+ if (event === "session-start") return emit2("SessionStart", await sessionStart(payload, full));
1575
+ if (event === "tool-failure") return emit2("PostToolUseFailure", await toolFailure(payload, full));
1576
+ } catch (e) {
1577
+ console.error(`codexguild hook ${event}: ${e instanceof Error ? e.message : e}`);
1578
+ }
1579
+ return 0;
1580
+ }
1581
+
1402
1582
  // src/code-scan.ts
1403
- import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "node:fs/promises";
1404
- import { extname, join as join4, relative as relative3 } from "node:path";
1583
+ import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
1584
+ import { extname, join as join5, relative as relative3 } from "node:path";
1405
1585
  var SOURCE_EXT = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".svelte", ".astro", ".py", ".go", ".rs"]);
1406
1586
  var LIMITS = { files: 5e3, fileBytes: 1024 * 1024, hits: 60 };
1407
1587
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1428,9 +1608,9 @@ async function sourceFiles(root) {
1428
1608
  if (out.length >= LIMITS.files) return;
1429
1609
  if (e.isDirectory()) {
1430
1610
  if (e.name.startsWith(".") || SKIP_DIRS2.has(e.name)) continue;
1431
- await walk(join4(dir, e.name), depth + 1);
1611
+ await walk(join5(dir, e.name), depth + 1);
1432
1612
  } else if (e.isFile() && SOURCE_EXT.has(extname(e.name)) && !/\.(min|d)\.[cm]?[jt]s$/.test(e.name)) {
1433
- out.push(join4(dir, e.name));
1613
+ out.push(join5(dir, e.name));
1434
1614
  }
1435
1615
  }
1436
1616
  };
@@ -1447,7 +1627,7 @@ async function scanCode(root, deps) {
1447
1627
  if (hits.length >= LIMITS.hits) break;
1448
1628
  const size = (await stat3(f).catch(() => null))?.size ?? 0;
1449
1629
  if (!size || size > LIMITS.fileBytes) continue;
1450
- const lines = (await readFile3(f, "utf8").catch(() => "")).split("\n");
1630
+ const lines = (await readFile4(f, "utf8").catch(() => "")).split("\n");
1451
1631
  lines.forEach((text2, i) => {
1452
1632
  for (const t of targets) {
1453
1633
  if (hits.length >= LIMITS.hits || !t.re.test(text2)) continue;
@@ -1501,11 +1681,12 @@ function formatCodeScan(hits, files, checked) {
1501
1681
  // src/index.ts
1502
1682
  var API = (process.env.CODEXGUILD_API_URL || "https://api.codexguild.com/v1").replace(/\/$/, "");
1503
1683
  var KEY = process.env.CODEXGUILD_API_KEY ?? "";
1504
- var VERSION = "1.7.0";
1505
- if (process.argv[2] === "guard") process.exit(await runGuard({ api: API, key: KEY }));
1684
+ var VERSION = "1.9.0";
1685
+ if (process.argv[2] === "guard") process.exit(await runGuard({ api: API, key: KEY || await findKey() }));
1686
+ if (process.argv[2] === "hook") process.exit(await runHook(process.argv[3], { api: API, key: KEY, version: VERSION }));
1506
1687
  var INSTRUCTION_FILES = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", "AGENTS.override.md", ".hermes.md", "HERMES.md", "GEMINI.md", ".cursorrules"];
1507
1688
  async function existingInstructionFiles(cwd) {
1508
- const present = await Promise.all(INSTRUCTION_FILES.map((f) => access(join5(cwd, f)).then(() => f, () => null)));
1689
+ const present = await Promise.all(INSTRUCTION_FILES.map((f) => access(join6(cwd, f)).then(() => f, () => null)));
1509
1690
  return present.filter((f) => f !== null);
1510
1691
  }
1511
1692
  var remote = null;
@@ -1514,8 +1695,8 @@ function remoteClient() {
1514
1695
  if (!KEY) return Promise.reject(new Error("CODEXGUILD_API_KEY is not set \u2014 create an agent key in the CodexGuild dashboard."));
1515
1696
  if (keyRejected) return Promise.reject(new Error(KEY_REJECTED));
1516
1697
  remote ??= (async () => {
1517
- const client = new Client({ name: "codexguild-local", version: VERSION });
1518
- await client.connect(new StreamableHTTPClientTransport(new URL(`${API}/mcp`), { requestInit: { headers: { Authorization: `Bearer ${KEY}` } } }));
1698
+ const client = new Client2({ name: "codexguild-local", version: VERSION });
1699
+ await client.connect(new StreamableHTTPClientTransport2(new URL(`${API}/mcp`), { requestInit: { headers: { Authorization: `Bearer ${KEY}` } } }));
1519
1700
  return client;
1520
1701
  })().catch((e) => {
1521
1702
  remote = null;
@@ -1545,6 +1726,8 @@ var ENHANCED = {
1545
1726
  codexguild_advise: "Runs locally first: stack, installed skills and the instruction files present in the project are detected automatically; you only need harness.",
1546
1727
  codexguild_debug: "Runs locally first: the project's package versions are read from its lockfiles and sent with the error \u2014 you only need error (and stack).",
1547
1728
  codexguild_upgrade_plan: "Runs locally first: `from` (and the ecosystem) is read from the lockfile when you only give `package`.",
1729
+ codexguild_note_save: "Runs locally first: `repo` is read from the git remote of the current repository \u2014 you only need text and kind.",
1730
+ codexguild_notes: "Runs locally first: `repo` is read from the git remote of the current repository.",
1548
1731
  codexguild_fix_submit: "Runs locally first: `packages` (names and versions after your changes) is read from the project's manifests and lockfiles \u2014 you only need task_id and summary. Run the package manager first so the lockfile is current."
1549
1732
  };
1550
1733
  function enhance(t) {
@@ -1559,7 +1742,8 @@ ${note}`,
1559
1742
  properties: { ...t.inputSchema.properties, cwd: { type: "string", description: "Project directory (defaults to the current directory)" } },
1560
1743
  // Filled from the lockfile here, so the agent need not pass it.
1561
1744
  ...t.name === "codexguild_upgrade_plan" && Array.isArray(t.inputSchema.required) ? { required: t.inputSchema.required.filter((r) => r !== "from") } : {},
1562
- ...t.name === "codexguild_fix_submit" && Array.isArray(t.inputSchema.required) ? { required: t.inputSchema.required.filter((r) => r !== "packages") } : {}
1745
+ ...t.name === "codexguild_fix_submit" && Array.isArray(t.inputSchema.required) ? { required: t.inputSchema.required.filter((r) => r !== "packages") } : {},
1746
+ ...(t.name === "codexguild_note_save" || t.name === "codexguild_notes") && Array.isArray(t.inputSchema.required) ? { required: t.inputSchema.required.filter((r) => r !== "repo") } : {}
1563
1747
  }
1564
1748
  };
1565
1749
  }
@@ -1575,11 +1759,12 @@ async function detected(name, cwd, explicit = {}) {
1575
1759
  const packages = deps2.slice(0, 300).map((d) => ({ name: d.name, ...d.version ? { version: d.version } : {}, ...d.ecosystem ? { ecosystem: d.ecosystem } : {}, ...d.runtime ? { runtime: true } : {} }));
1576
1760
  return { args: { packages }, summary: `CodexGuild: submitted ${packages.length} dependencies from ${lockfiles2.length ? lockfiles2.join(", ") : "manifests (no lockfile \u2014 versions are ranges)"}.` };
1577
1761
  }
1762
+ if (name === "codexguild_note_save" || name === "codexguild_notes") return { args: { repo: await repoKey(cwd) } };
1578
1763
  if (name === "codexguild_debug") {
1579
1764
  const { deps: deps2 } = await detectStack2(cwd);
1580
1765
  return { args: { packages: deps2.slice(0, 300).map((d) => ({ name: d.name, ...d.version ? { version: d.version } : {}, ...d.ecosystem ? { ecosystem: d.ecosystem } : {} })) } };
1581
1766
  }
1582
- const [{ deps, manifests, lockfiles, exact, ranges }, local, project] = await Promise.all([detectStack2(cwd), scanLocalSkills(), projectKey(cwd)]);
1767
+ const [{ deps, manifests, lockfiles, exact, ranges }, local, project, repo] = await Promise.all([detectStack2(cwd), scanLocalSkills(), projectKey(cwd), repoKey(cwd)]);
1583
1768
  const installedSkills = await installedSkillNames(local.reports.map((r) => r.dir));
1584
1769
  if (name === "codexguild_advise") {
1585
1770
  return { args: { stack: deps, installedSkills, existingInstructionFiles: await existingInstructionFiles(cwd), project } };
@@ -1592,7 +1777,7 @@ async function detected(name, cwd, explicit = {}) {
1592
1777
  ...flagged.slice(0, 10).map((x) => ` - FLAGGED ${x.dir}: ${x.result.findings[0]?.title ?? ""}`),
1593
1778
  ...flagged.length ? [" Run codexguild_scan_skills for details and ask the user before using flagged skills."] : []
1594
1779
  ].join("\n");
1595
- return { args: { stack: deps, installedSkills, project }, summary };
1780
+ return { args: { stack: deps, installedSkills, project, repo }, summary };
1596
1781
  }
1597
1782
  var LINT_TOOL = {
1598
1783
  name: "codexguild_lint_instructions",
@@ -1607,7 +1792,7 @@ async function lintLocal(cwd, files) {
1607
1792
  if (!targets.length) return "No instruction files found (looked for CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, \u2026).";
1608
1793
  const out = [];
1609
1794
  for (const f of targets) {
1610
- const content = await readFile4(isAbsolute2(f) ? f : join5(cwd, f), "utf8").catch(() => null);
1795
+ const content = await readFile5(isAbsolute2(f) ? f : join6(cwd, f), "utf8").catch(() => null);
1611
1796
  if (content === null) {
1612
1797
  out.push(`## ${f}
1613
1798
  Could not read the file.`);
@@ -1631,12 +1816,12 @@ var WORKFLOWS_TOOL = {
1631
1816
  annotations: { readOnlyHint: true, openWorldHint: false }
1632
1817
  };
1633
1818
  async function scanWorkflowsLocal(cwd, files) {
1634
- const dir = join5(cwd, ".github", "workflows");
1635
- const targets = files?.length ? files : (await readdir4(dir).catch(() => [])).filter((f) => /\.ya?ml$/.test(f)).map((f) => join5(dir, f));
1819
+ const dir = join6(cwd, ".github", "workflows");
1820
+ const targets = files?.length ? files : (await readdir4(dir).catch(() => [])).filter((f) => /\.ya?ml$/.test(f)).map((f) => join6(dir, f));
1636
1821
  if (!targets.length) return "No workflows found in .github/workflows.";
1637
1822
  const out = [];
1638
1823
  for (const f of targets) {
1639
- const content = await readFile4(isAbsolute2(f) ? f : join5(cwd, f), "utf8").catch(() => null);
1824
+ const content = await readFile5(isAbsolute2(f) ? f : join6(cwd, f), "utf8").catch(() => null);
1640
1825
  const name = f.startsWith(cwd + "/") ? f.slice(cwd.length + 1) : f;
1641
1826
  if (content === null) {
1642
1827
  out.push(`## ${name}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexguild/mcp",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "CodexGuild MCP server (stdio) \u2014 boosts your coding agent: remote CodexGuild tools plus local stack detection from lockfiles, deprecated APIs found in your own code, and offline security audits of skills, instructions and agent workflows",
5
5
  "keywords": [
6
6
  "mcp",