Skip to content

Add GitHub Copilot CLI + Antigravity CLI (agy) support#787

Open
ken-jo wants to merge 37 commits into
mksglu:nextfrom
ken-jo:feat/copilot-cli-antigravity-cli
Open

Add GitHub Copilot CLI + Antigravity CLI (agy) support#787
ken-jo wants to merge 37 commits into
mksglu:nextfrom
ken-jo:feat/copilot-cli-antigravity-cli

Conversation

@ken-jo

@ken-jo ken-jo commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

What / Why / How

This PR adds first-class support for two new CLI agents plus the cross-cutting fixes they both need. Resolves #774 (Antigravity CLI / agy integration) and #775 (official GitHub Copilot CLI support).

Note

🙏 Apology / context. My first attempt (#779) didn't actually work end-to-end — agy was mis-detected as Claude Code, and the agy MCP tools never reached the model. It also pulled in setup machinery that wasn't wanted. This PR is narrow (only the two adapters + the cross-cutting fixes they need) and I've now validated it on both Linux and a real Windows machine (agy 1.0.5–1.0.6 + GitHub Copilot CLI). Sorry for the earlier churn — please re-test this one using the install steps below.

What changed:

  • GitHub Copilot CLI adapter (src/adapters/copilot-cli/) — CopilotCliAdapter with COPILOT_HOME-aware config/session paths (mcp-config.json, hooks/context-mode.json, session DB root all follow COPILOT_HOME), a version: 1 flat hook schema (the CLI runtime requires it or hooks never fire), top-level decision output (permissionDecision/permissionDecisionReason/modifiedArgs/additionalContext — not the VS Code hookSpecificOutput wrapper), and six event dispatchers (PreToolUse routes/vetoes via the routing decision; PostToolUse, SessionStart, UserPromptSubmit, Stop, and PreCompact capture/restore through the shared session DB). The TS server's session reads are kept aligned with where the hook runtime writes so relocating COPILOT_HOME never orphans sessions.
  • Copilot CLI plugin bundle (configs/copilot-cli/) — a one-command copilot plugin install mksglu/context-mode:configs/copilot-cli that registers the MCP server, the routing skill, and all six capture hooks in a single shot (no follow-up context-mode upgrade or agent round-trip). The bundled .mcp.json pins CONTEXT_MODE_PLATFORM=copilot-cli so detection short-circuits to the explicit-override tier and self-identifies as Copilot even when Claude Code is co-installed. A single .gitignore negation (!configs/copilot-cli/.mcp.json) un-ignores exactly that one committed config, since a Copilot plugin has no other way to declare MCP.
  • Antigravity CLI (agy) adapter + plugin (src/adapters/antigravity-cli/, configs/antigravity-cli/) — AntigravityCliAdapter subclasses the IDE AntigravityAdapter but targets agy's distinct global layout (config dir ~/.gemini/antigravity-cli, hooks ~/.gemini/config/hooks.json). agy's plugin system is Claude-compatible, so the bundle is .claude-plugin/plugin.json (identity + skill) + a root .mcp.json + hooks/hooks.json. agy plugin install <bundle> registers all of it in one command — verified on agy 1.0.6 it logs mcpServers : 1 processed and writes the server (env preserved) into its plugin profile ~/.gemini/config/plugins/context-mode/mcp_config.json. The bundle's .mcp.json pins CONTEXT_MODE_PLATFORM=antigravity-cli, so the server self-identifies as agy — fixing the [Feature]: Antigravity-cli (agy) better integration #774 mis-detection at the MCP level, not only via the dir-marker probes. The cross-platform Node installer (npm run install:agy, replacing the former bash-only script) wraps the install to also clear agy's stale per-server tool-schema cache (~/.gemini/antigravity-cli/mcp/context-mode/, which agy never refreshes) so the ctx_* tools reappear, and to self-verify the MCP registration. Like the Copilot bundle, this .mcp.json is the one committed instance, un-ignored by a single .gitignore negation. agy honors no PreToolUse stdout veto in auto-run mode, so the routing skill is the primary enforcement and the hook is capture-only.
  • Gemini-safe schema sanitizer (src/server.ts) — sanitizeSchemaForStrictClients() rewrites the emitted tools/list schema so Gemini's function-calling API (used by Antigravity CLI (agy) and Gemini CLI) accepts it: every const: X becomes enum: [X] and additionalProperties is stripped. Both rewrites are behavior-preserving (a one-value enum equals const; additionalProperties is advisory because every ctx_* handler re-parses args with Zod). Without this, Gemini-family hosts silently drop every ctx_* tool and the agent falls back to hand-rolling MCP through Bash — so this restores ctx_* exposure on the existing Gemini CLI platform too, not just on the two new CLIs. installStrictClientSchemaCompat() wraps the SDK's tools/list handler best-effort (try/catch), so an SDK refactor degrades rather than breaks.
  • Detection + fail-open dispatch (src/adapters/detect.ts, src/adapters/client-map.ts, src/cli.ts) — new config-dir marker probes run before the generic ~/.claude/~/.gemini fallbacks (the core of [Feature]: Antigravity-cli (agy) better integration #774: a gemini-cli→agy migration leaves both dirs, so ~/.claude otherwise mis-detected agy as Claude Code), and client-map.ts routes the clientInfo name (agyantigravity-cli, GitHub Copilot CLIcopilot-cli). The copilot-cli probe only matches a context-mode-written file (mcp-config.json or hooks/context-mode.json), never a bare ~/.copilot, so a co-installed-but-unconfigured Copilot user stays on Claude Code. hookDispatch() now fails open (exit(0) instead of exit(1)) on an unknown platform/event, because a version-skewed older global binary that exits 1 with empty stdout makes Copilot CLI 1.0.59 treat it as a hook error and deny every tool ("Denied by preToolUse hook (hook errored)"), bricking the agent.
  • Supporting glue — a tolerant JSONC parser (src/util/jsonc.ts, stripJsonComments/parseJsonc) that both new adapters import to read agy/Copilot configs (which ship comments) without false-failing JSON.parse; the copilot-cli top-level decision formatter and tool-prefix entries (hooks/core/formatters.mjs, hooks/core/tool-naming.mjs); per-platform COPILOT_OPTS/ANTIGRAVITY_CLI_OPTS capture wiring (hooks/session-helpers.mjs); and package.json + scripts/version-sync.mjs updates so the agy plugin manifest version stays in sync.

Source note (same as #779): GitHub Copilot CLI is source-available (not OSS); the installed agy is a binary with no published source. So agy/copilot behavior was validated by running the real binaries and capturing MCP traffic, not by reading their source.

Affected platforms

  • Claude Code
  • Cursor
  • VS Code Copilot (GitHub Copilot)
  • JetBrains Copilot
  • Gemini CLI
  • Qwen Code
  • OpenCode
  • KiloCode
  • Codex CLI
  • OpenClaw (Pi Agent)
  • Pi
  • Kiro
  • Antigravity
  • Zed
  • All platforms
  • GitHub Copilot CLI
  • Antigravity CLI (agy)

(Gemini CLI is checked because the schema sanitizer restores ctx_* tool exposure on every Gemini-family function-calling host, including the existing Gemini CLI platform — not only the two new CLIs.)

Test plan

Linux — unit/integration suite (npm test):

  • 5 new test files: tests/adapters/copilot-cli.test.ts (adapter unit: json-stdio paradigm, COPILOT_HOME-aware paths, flat version:1 idempotent hook config, snake_case input parsing, top-level decision output, checkPluginRegistration remediation), tests/adapters/copilot-cli-plugin.test.ts (bundle: committed .mcp.json that git check-ignore does not ignore, CONTEXT_MODE_PLATFORM=copilot-cli env pin, six-event hooks.json version=1), tests/adapters/antigravity-cli-plugin.test.ts (agy bundle: committed env-pinned .mcp.json that git check-ignore does not ignore, no manifest mcpServers, capture-only PostToolUse with no PreToolUse, schema-cache clear), tests/core/strict-client-schema.test.ts (sanitizer: constenum, additionalProperties stripped, nested recursion, input not mutated), tests/hooks/copilot-cli-capture.test.ts (spawns the real userpromptsubmit/stop/precompact dispatchers and asserts SQLite persistence — incl. the Stop createHash(undefined) regression guard).
  • 7 extended files: tests/adapters/antigravity.test.ts (full AntigravityCliAdapter suite, idempotent capture hook preserving unrelated hooks), tests/adapters/detect.test.ts + tests/adapters/detect-config-dir.test.ts (clientInfo agy/GitHub Copilot CLI routing, [Feature]: Antigravity-cli (agy) better integration #774 ordering — dedicated markers beat ~/.claude/~/.gemini, bare ~/.copilot does not), tests/core/cli.test.ts (fail-open: unmapped hook exits 0), tests/integration/project-dir-strict.test.ts, tests/scripts/version-sync.test.ts, tests/session/multi-adapter-stats.test.ts (15→17 adapters).
  • Build artifacts: the committed esbuild bundles (server.bundle.mjs, cli.bundle.mjs, hooks/session-db.bundle.mjs, hooks/session-extract.bundle.mjs, hooks/security.bundle.mjs) were regenerated so the published runtime carries the sanitizer + both new adapters/hooks.

Windows — live validation (real binaries):

  • Copilot CLI plugin — real remote install: copilot plugin install <owner>/<repo>:configs/copilot-cli (from a repo's default branch) extracted the bundle's .mcp.json + hooks.json + routing skill (copilot plugin list then shows context-mode), and ctx_execute returned 42 — the documented one-command install verified end-to-end against a real GitHub install, not only a local proxy.
  • Copilot CLI plugin — hooks fire: loading the same bundle via copilot --plugin-dir (local), the PostToolUse hook captured a non-MCP bash tool's output into the session DB — direct proof that hooks fire, not just that MCP loads.
  • Env pin: with the bundle's CONTEXT_MODE_PLATFORM=copilot-cli, ctx_upgrade/detection resolved copilot-cli and left the co-installed Claude Code config untouched.
  • Copilot CLI manual path (copilot mcp add + context-mode upgrade writing the version:1 hooks file) also verified end-to-end.
  • Antigravity CLI via npm run install:agy (→ agy plugin install configs/antigravity-cli) on agy 1.0.6: the bundle's .mcp.json registered mcpServers : 1 processed (env CONTEXT_MODE_PLATFORM=antigravity-cli preserved) into agy's plugin profile, alongside the routing skill + capture hook, with the stale tool-schema cache cleared. Runtime confirmed: agy -p "...ctx_execute...7 + 5..." → 12 — the plugin-scoped MCP resolves in a real agy session with no global-profile entry. context-mode doctor recognizes that plugin-scoped registration (MCP + hook), not just the global paths. The earlier (pre-[Feature]: Antigravity-cli (agy) better integration #774) failure mode was agy mis-detected as Claude Code with its ctx_* tools dropped; both are fixed (env pin + Gemini-safe schema). Note: agy has no owner/repo remote install (only a local path or a full git URL, which clones the repo root and skips a subdirectory bundle), so the local npm run install:agy is the supported install — unlike Copilot's owner/repo:path.

GitHub Copilot CLI

npm install -g context-mode
# Recommended — one command (MCP + 6 hooks + routing skill), no agent round-trip:
copilot plugin install mksglu/context-mode:configs/copilot-cli
# restart copilot, then verify:
copilot -p "Use the context-mode ctx_execute MCP tool to compute 6 times 7." --allow-all   # -> 42

# Alternative (no plugin) — register MCP + the version:1 hooks file manually:
copilot mcp add context-mode --env CONTEXT_MODE_PLATFORM=copilot-cli -- context-mode   # MCP -> ~/.copilot/mcp-config.json
CONTEXT_MODE_PLATFORM=copilot-cli context-mode upgrade                                  # hooks -> ~/.copilot/hooks/context-mode.json
  • The --env CONTEXT_MODE_PLATFORM=copilot-cli pin (and running context-mode upgrade with that var set, or from inside a Copilot session) keeps detection on copilot-cli even when Gemini CLI / Claude Code are co-installed. Without the pin, detection falls back to the ~/.copilot/mcp-config.json marker probe, which still resolves correctly but is more fragile when other CLI config dirs are present.
  • COPILOT_HOME relocates the hook config, MCP config, and the context-mode session-DB root.
  • copilot doctor equivalent: context-mode doctor should report platform copilot-cli and the 6 hooks.

Antigravity CLI (agy)

npm install -g context-mode                     # the plugin's MCP server runs the global binary
git clone https://github.com/mksglu/context-mode.git
cd context-mode
npm run install:agy                             # cross-platform Node — Windows/macOS/Linux, no bash
# restart agy, then verify:
agy -p "Use the context-mode ctx_execute MCP tool to compute 7 + 5. Answer only the number." --dangerously-skip-permissions   # -> 12

npm run install:agy wraps agy plugin install configs/antigravity-cli — which registers the MCP server (from the bundle's .mcp.json), the routing skill, and the PostToolUse capture hook in one step — then clears agy's stale tool-schema cache (~/.gemini/antigravity-cli/mcp/context-mode/) so the model sees the current ctx_* tools, and self-verifies that the MCP landed in agy's plugin profile. Manual fallback if a future agy build skips it: add {"command":"context-mode"} under mcpServers in ~/.gemini/config/mcp_config.json.

Checklist

  • Tests added/updated (TDD: red → green)
  • npm test passes
  • npm run typecheck passes
  • Docs updated if needed (README, platform-support.md)
  • No Windows path regressions (forward slashes only)
  • Targets next branch (unless hotfix)
Cross-platform notes

Our CI runs on Ubuntu, macOS, and Windows.

  • If touching file paths, verify forward-slash normalization on Windows
  • If touching hook paths, verify no backslash separators
  • Use path.join() / path.resolve(), never hardcode / separators
  • Use event-based stdin reading — readFileSync(0) breaks on Windows
  • Use os.tmpdir(), never hardcode /tmp

The agy installer was rewritten from bash to cross-platform Node specifically so it runs on Windows; the Copilot CLI adapter and both capture hooks use path-based joins and event-based stdin throughout, and the Windows live validation above exercised both new platforms on a real Windows machine.

ken-jo and others added 12 commits June 5, 2026 06:17
Add two agentic CLI adapters onto next's existing adapter registration —
without the abandoned PR's setup subcommand / consolidated registry.

Antigravity CLI (agy):
- MCP + capture-only PostToolUse hook adapter (agy honors no stdout veto in
  auto-run mode; verified against agy 1.0.5). The agy hook payload
  {conversationId, toolCall, workspacePaths} is mapped onto the shared
  capture pipeline.
- Ships a Claude-layout plugin bundle (configs/antigravity-cli/) installed via
  `npm run install:agy` (mirrors install:openclaw), with a version-skew
  capture-hook probe in the installer.

GitHub Copilot CLI (1.0.59):
- json-stdio hook adapter with six events: PreToolUse, PostToolUse, PreCompact,
  SessionStart, UserPromptSubmit, Stop. Overrides CopilotBaseAdapter to emit the
  FLAT {type,command} + top-level "version": 1 hook config Copilot CLI requires.
- MCP install via `copilot mcp add context-mode -- context-mode`.
- Fix a latent Stop-hook bug: a session_end event with no `data` threw inside
  insertEvent (createHash(undefined)) and was silently dropped.

Cross-cutting:
- mksglu#774: probe agy/copilot config markers before the generic ~/.claude check.
  The copilot marker is narrowed to context-mode-written files
  (~/.copilot/mcp-config.json | hooks/context-mode.json), not a bare ~/.copilot/
  dir, so a co-installed-but-unconfigured Copilot CLI cannot steal detection
  from a Claude Code user.
- Dispatcher fails OPEN (exit 0) on a missing hook script: GitHub Copilot CLI
  treats an exit-1 PreToolUse hook as DENY, so a version skew (a newer adapter's
  hook command on an older global) would otherwise brick the agent.

Fixes mksglu#774. Fixes mksglu#775.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the new HOOK_MAP entries, client-map keys, validPlatforms,
getSessionDirSegments cases, and the fail-open dispatcher into the
esbuild-generated runtime bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Kiro classification

Make README.md and docs/platform-support.md internally consistent and aligned
with the adapter source of truth.

Header sync (18 platforms everywhere):
- The Main Comparison Table (was 11 cols), the Capability Matrix (was 11), and
  the README Platform Compatibility table (was 17, missing Kimi Code) now list
  the SAME 18 platforms in one shared order. Adds the two branch-new platforms
  (GitHub Copilot CLI, Antigravity CLI `agy`) plus previously-omitted Qwen Code,
  KiloCode, OpenClaw, Zed, Pi as columns. Each cell sourced from the per-platform
  detail sections / adapter source and independently verified.
- Fix five ragged rows in the Main Comparison Table (a dropped trailing OMP cell)
  and add CLI Hook Dispatcher rows for qwen-code + copilot-cli.
- GitHub Copilot CLI section: normalize the `**Hook Names:**` label and add the
  missing `**Output Modification:**` field for json-stdio-family parity.

Fix stale Kiro classification (code is the source of truth):
- The kiro adapter is json-stdio with working preToolUse/postToolUse hooks
  (hooks/kiro/{pretooluse,posttooluse}.mjs + a kiro HOOK_MAP entry), yet the docs
  called it "MCP-only (Phase 2 — not implemented)" and the README contradicted
  itself ("no hook support" in one place, "native preToolUse/postToolUse" in two
  others).
- Reclassify Kiro as json-stdio with PreToolUse + PostToolUse + exit-code-2
  blocking across the Overview paradigm table, both wide tables, the dispatcher
  table, and the Kiro detail section; document that agentSpawn (SessionStart) and
  stop are not yet wired, so session restore after compaction is unavailable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fresh clones

The agy plugin-bundle test asserted configs/antigravity-cli/.mcp.json, but
.mcp.json is gitignored repo-wide and was never committed — so the test passed
on the dev machine (file present locally) yet failed on a fresh clone with
ENOENT. Committing the file is the wrong fix: the .gitignore comment documents
that shipping .mcp.json has silently broken fresh installs before (mksglu#253/mksglu#531).

- The bundle declares MCP the Claude way via .claude-plugin/plugin.json
  mcpServers (committed — the mechanism agy reads on `agy plugin install`),
  mirrored by the agy-native mcp_config.json (committed). Remove the vestigial
  bundle .mcp.json and stop the test + docs from requiring it. Every file the
  plugin test reads is now git-tracked, so a fresh clone passes.
- README: Kiro was still grouped under "Non-hook platforms" in the routing-
  enforcement note. Kiro has native preToolUse/postToolUse hooks; it needs the
  manual KIRO.md copy only because agentSpawn/SessionStart is not yet wired.
  Reword to say so.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…E parity

Windows fix (real): replace the bash-only agy plugin installer with a
cross-platform Node script so `npm run install:agy` runs natively on Windows
(PowerShell/cmd), not just Git Bash/WSL. agy runs on Windows, so its installer
must too — the old `node -e` wrapper hard-exited 1 on win32. openclaw stays
bash-only (it is genuinely POSIX-only). Removes
scripts/install-antigravity-cli-plugin.sh in favor of
scripts/install-antigravity-cli-plugin.mjs (same preflight + version-skew probe).

copilot-cli hardening (COPILOT_HOME edge case only — the default ~/.copilot
install was and remains correct):
- CopilotCliAdapter.getSessionDir() now roots at getConfigDir() (COPILOT_HOME-
  aware), mirroring codex/kimi, so the TS server reads sessions from the same
  place the hook runtime (COPILOT_OPTS configDirEnv: COPILOT_HOME) writes them.
  Previously a relocated COPILOT_HOME split hook writes ($COPILOT_HOME/...) from
  server reads (~/.copilot/...), making sessions appear empty.
- detect.ts copilot-cli marker honors COPILOT_HOME, not just ~/.copilot.

No change to the default (COPILOT_HOME-unset) behavior; a regression test pins
both the ~/.copilot default and the COPILOT_HOME-rooted path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up CopilotCliAdapter.getSessionDir() and the COPILOT_HOME-aware detect.ts
marker into the esbuild runtime bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…install skips it)

`npm run install:agy` ran only `agy plugin install`, which — verified against
agy 1.0.5 — processes a bundle's skills + hooks but logs "mcpServers : skipped
(not found)" and registers NO MCP server. agy reads a plugin's MCP only from a
bundle `.mcp.json` (intentionally not shipped — gitignored repo-wide after
mksglu#253/mksglu#531) and has no `agy mcp add` command, so context-mode's MCP server was
never registered: users had to add it to ~/.gemini/config/mcp_config.json by hand
(reported on Windows; reproduced on Linux: `mcpServers : skipped (not found)`).

The installer now also writes context-mode into agy's GLOBAL MCP profile
~/.gemini/config/mcp_config.json (idempotent JSON merge, preserves other servers,
tolerates a malformed file) — the file agy actually loads and `context-mode
doctor` checks. Verified end-to-end on agy 1.0.5: `npm run install:agy` →
mcp_config.json gains context-mode → `agy -p "... ctx_execute ... 7 + 5"` → 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tx_* tools

Antigravity CLI (agy) and Gemini CLI use Gemini's function-calling API, which
rejects JSON Schema `const` and `additionalProperties`. When a tool's parameter
schema contains either, the host SILENTLY DROPS that tool from the model's
function list — so agy never sees the ctx_* tools and works around them by
hand-rolling the MCP protocol through its Bash tool (verified on Windows: agy
wrote scratch/call_ctx_stats.js + list_mcp_tools.js MCP clients instead of
calling the tools natively). That defeats the point of context-mode — bash
output floods the context window instead of staying in the sandbox.

context-mode builds schemas with Zod, which emits `const` (from coerce/preprocess
constructs) and `additionalProperties`, with no Gemini sanitization. Wrap the
SDK's tools/list handler to rewrite the EMITTED schema:
  - `const: X` -> `enum: [X]`   (an identical single-value constraint)
  - drop `additionalProperties` (advisory-only; every ctx_* handler parses args
    with Zod, which strips unknown keys server-side regardless)

Both transforms are behavior-preserving for every other client (Claude Code,
Copilot, Cursor): const and a one-value enum are equivalent, and no model sends
undeclared properties — only the wire schema changes, never validation or how a
tool is called. Best-effort: if the MCP SDK internals shift, the original handler
is left untouched (no regression). Verified on the real tools/list: all 11 ctx_*
tools now emit 0 `const` / 0 `additionalProperties`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agy caches each MCP server's tool schemas under
~/.gemini/antigravity-cli/mcp/<server>/ and does NOT refresh them on reconnect
(verified on agy 1.0.6 against a live Windows install). A cache captured by a
context-mode older than the Gemini-safe-schema fix (ae6e7d3) keeps the
`const` / `additionalProperties` schemas that make Antigravity CLI silently drop
the ctx_* tools from the model's function list — so the schema fix never reaches
the model and the agent keeps working around the tools via shell scripts.

The installer now clears that cache after registering the MCP server, so agy
re-fetches the current Gemini-safe tools/list on its next launch. Verified on
Windows: clearing the cache + reconnecting makes agy re-store ctx_execute.json
with 0 `const` / 0 `additionalProperties`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lot COPILOT_HOME

Reflect this branch's recent behavior changes in the support docs:
- agy: context-mode emits Gemini-safe tool schemas (const->enum, additionalProperties
  stripped) so Antigravity CLI exposes the ctx_* tools instead of silently dropping
  them; agy caches tool schemas and never refreshes them, so `npm run install:agy`
  clears that cache. Added to the agy Known Issues + install steps (platform-support.md
  + README).
- copilot-cli: COPILOT_HOME now relocates the session-DB root too (getSessionDir honors
  it), and the detection marker honors COPILOT_HOME.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t MCP + hooks)

The README + platform-support docs claimed Copilot CLI plugins register only
skills/agents — not MCP servers or hooks. That's wrong: `copilot plugin --help`
and `copilot mcp --help` (Copilot CLI 1.x) confirm a plugin can register MCP
servers (a `.mcp.json` in the plugin root or `.github/mcp.json`) and hooks
(`hooks.json`), installed in one command via `copilot plugin install owner/repo:path`
(from a GitHub repo subdirectory, no clone). The "direct installs deprecated for
plugin@marketplace" note was also inaccurate (all source forms are current).

Corrected both docs. context-mode still registers via `copilot mcp add` +
`context-mode upgrade` today; a shippable Copilot plugin bundle
(configs/copilot-cli/) is noted as a planned follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ken-jo ken-jo marked this pull request as draft June 6, 2026 05:22
ken-jo and others added 3 commits June 6, 2026 06:16
…ll, phase 1)

`copilot plugin install mksglu/context-mode:configs/copilot-cli` registers the
context-mode MCP server + routing skill in one command — no `context-mode
upgrade` / agent call.

The bundle's .mcp.json pins CONTEXT_MODE_PLATFORM=copilot-cli so the server
self-identifies as Copilot. This fixes the detection trap where a co-installed
Claude Code (~/.claude/plugins/installed_plugins.json) makes standalone
`context-mode upgrade` — and even ctx_upgrade — resolve claude-code and write
Claude's config instead of Copilot's.

Real Copilot plugins discover MCP from a root `.mcp.json`, so this is the one
bundle whose .mcp.json is committed: .gitignore un-ignores exactly this path
(the repo-wide ignore from mksglu#253/mksglu#531 guards the repo-ROOT dev file, not a
plugin's own config).

Phase 2 (capture hooks via the plugin's hooks.json) follows once its format is
verified on Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (phase 2)

configs/copilot-cli/hooks.json registers all six Copilot hook events
(PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop, PreCompact),
each dispatching `context-mode hook copilot-cli <event>` against the global
binary. It is byte-equivalent to what `context-mode upgrade` writes to
~/.copilot/hooks/context-mode.json (the format verified against the
@github/copilot binary), so `copilot plugin install …:configs/copilot-cli` now
registers MCP + skill + capture hooks in one command — no `upgrade` / agent call.

Verified on Windows: with the plugin's env-pinned MCP config + a current global
context-mode, Copilot calls ctx_execute (→ 12) and ctx_upgrade resolves
copilot-cli (writes the Copilot hook, leaves Claude Code's config untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README + platform-support now lead with `copilot plugin install
mksglu/context-mode:configs/copilot-cli` (one command: MCP + hooks + skill, no
upgrade/agent call), keeping `copilot mcp add` + `context-mode upgrade` as the
manual no-plugin path. Notes the .mcp.json env pin (CONTEXT_MODE_PLATFORM=
copilot-cli) that fixes detection under a co-installed Claude Code, the
.gitignore un-ignore for the bundle's .mcp.json, and the `copilot --plugin-dir`
local-test path. Drops the earlier "planned follow-up" wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ken-jo

ken-jo commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Update — GitHub Copilot CLI plugin bundle (configs/copilot-cli/)

Per the review note, Copilot CLI now installs as a one-command plugin (no context-mode upgrade / agent call), alongside the existing manual copilot mcp add path:

copilot plugin install mksglu/context-mode:configs/copilot-cli

The bundle ships the files real Copilot marketplace plugins use:

  • .mcp.json — the MCP server, pinned with CONTEXT_MODE_PLATFORM=copilot-cli
  • hooks.json — the six capture hooks (byte-equivalent to what context-mode upgrade writes to ~/.copilot/hooks/)
  • skills/context-mode/SKILL.md — the routing skill
  • .github/plugin/plugin.json — manifest

Why the env pin matters: on a machine where context-mode is also installed as a Claude Code plugin, ~/.claude/plugins/installed_plugins.json makes platform detection resolve claude-code — so even ctx_upgrade would write Claude's config instead of Copilot's. Pinning CONTEXT_MODE_PLATFORM=copilot-cli in the plugin's .mcp.json makes the server self-identify as Copilot, so detection/ctx_upgrade resolve copilot-cli.

Why this .mcp.json is committed: a Copilot plugin discovers MCP only from a root .mcp.json, so .gitignore un-ignores exactly configs/copilot-cli/.mcp.json. The repo-wide .mcp.json ignore from #253/#531 guards the repo-root dev file, not a plugin's own config.

Windows validation (real Copilot CLI)

Loaded the local bundle as a plugin via copilot --plugin-dir <bundle> (same component-discovery as plugin install):

  • MCP: Copilot calls ctx_execute and it resolves.
  • Hooks fire: the PostToolUse hook captured a non-MCP bash tool's output into the session DB — proof the hook ran, since the MCP server never sees bash tools.
  • Detection: with the env pin, ctx_upgrade resolves copilot-cli and writes the Copilot hook, leaving a co-installed Claude Code's config untouched.
  • ✅ The manual path (copilot mcp add + context-mode upgrade) also verified: MCP responds and the standalone hooks fire independently of any plugin.

New guard tests: tests/adapters/copilot-cli-plugin.test.ts (5 passing).

ken-jo and others added 3 commits June 6, 2026 07:29
…isters MCP directly

The agy bundle declared MCP in two places that nothing consumed — a `mcpServers`
block in .claude-plugin/plugin.json (which `agy plugin install` SKIPS) and a dead
mcp_config.json (read by no code) — and relied on the installer writing agy's
GLOBAL ~/.gemini/config/mcp_config.json as a workaround for not shipping .mcp.json.

agy's plugin system is Claude-compatible and reads MCP from a bundle `.mcp.json`,
exactly like the Copilot bundle. Verified on agy 1.0.6: `agy plugin install` with
a bundle .mcp.json logs "mcpServers : 1 processed" and registers the server (env
preserved) into ~/.gemini/config/plugins/<name>/mcp_config.json. So:

- ship configs/antigravity-cli/.mcp.json (un-ignored via a .gitignore negation),
  pinning CONTEXT_MODE_PLATFORM=antigravity-cli so the server self-identifies as
  agy — fixing the mksglu#774 mis-detection at the MCP level, not only via dir markers;
- drop the dead mcp_config.json and the manifest's redundant mcpServers;
- simplify the installer: `agy plugin install` now registers MCP + skill + hook;
  it keeps the stale tool-schema cache-clear + version-skew probe, and now
  self-verifies the plugin-scoped MCP registration (one-line manual fallback if a
  future agy skips it) instead of blindly writing the global profile.

Both CLI plugin bundles (copilot-cli, antigravity-cli) are now consistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…registration

After the bundle moved to `.mcp.json` (so `agy plugin install` registers MCP +
the capture hook into agy's plugin profile ~/.gemini/config/plugins/context-mode/),
doctor still only checked the global ~/.gemini/config/{mcp_config,hooks}.json and
warned "context-mode not found" / "capture hook not configured" on a working install.

- checkPluginRegistration + validateHooks now accept the plugin profile (the
  canonical `agy plugin install` location) OR the global path (manual fallback).
- getInstalledVersion reads the installed plugin.json version so the version line
  shows a real semver (PASS when current) instead of the bogus "vconfigured".
- fix hints point to `npm run install:agy`.

Unit-tested (plugin-scoped PASS for both MCP + hook). Runtime already confirmed on
agy 1.0.6: `npm run install:agy` + `agy -p "...ctx_execute...7+5..."` → 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ken-jo ken-jo marked this pull request as ready for review June 6, 2026 11:13
@mksglu mksglu requested a review from ousamabenyounes June 6, 2026 21:10
@ousamabenyounes

Copy link
Copy Markdown
Collaborator

Thanks for the careful re-submission @ken-jo — the bundle regeneration is in sync this time (assert-bundle OK), the fail-open exit(0) reasoning is solid, and the COPILOT_HOME mirroring between copilotCliHome() and detect.ts is a nice touch. I ran the full suite locally on this branch: 4255 passed / 43 skipped, exit 0, build clean. 🎉

Sharing three medium-severity items I hit during review, each with a suggested patch — none are blockers, but #1 is worth fixing before merge. 🙏


🟠 1. hooks/copilot-cli/pretooluse.mjs — unguarded body can brick the agent

The hook body isn't wrapped in try/catch (the top-level await initSecurity and lines 14–30). Any throw → non-zero exit + empty stdout. By your own note in cli.ts, Copilot CLI 1.0.59 treats that as "hook errored" and denies every tool ("Denied by preToolUse hook") — which is exactly the brick mode hookDispatch() already guards against. So the fail-open you added there is undone if the hook itself throws.

It matches the existing convention (codex/kimi/vscode are unguarded too), but copilot-cli is the one platform documented to brick on error, so it deserves a fail-open wrapper:

const __hookDir = dirname(fileURLToPath(import.meta.url));

try {
  await initSecurity(resolve(__hookDir, "..", "..", "build"));

  const raw = await readStdin();
  const input = parseStdin(raw);
  const tool = input.tool_name ?? input.toolName ?? "";
  const toolInput = input.tool_input ?? input.toolArgs ?? {};
  const projectDir = getInputProjectDir(input, COPILOT_OPTS);

  const decision = routePreToolUse(
    tool,
    toolInput,
    projectDir,
    "copilot-cli",
    getSessionId(input, COPILOT_OPTS),
  );
  const response = formatDecision("copilot-cli", decision);
  if (response !== null) {
    process.stdout.write(JSON.stringify(response) + "\n");
  }
} catch {
  // Fail OPEN — emit nothing so Copilot CLI ALLOWS the tool. An exit-non-zero +
  // empty-stdout PreToolUse hook is treated as "hook errored" by Copilot CLI
  // 1.0.59 and DENIES every tool — the same brick mode hookDispatch() guards.
}

🟠 2. src/util/jsonc.ts — trailing-comma strip corrupts string values

stripJsonComments is string-aware for comments, but the trailing-comma removal replace(/,(\s*[}\]])/g, "$1") (line 33) runs over the whole output, including string contents. Reproduced on this branch:

import { parseJsonc } from "./build/util/jsonc.js";
const jsonc = `{
  // comment forces the strip path
  "note": "array literal: [1, ]"
}`;
parseJsonc(jsonc).note;
//  actual:   "array literal: [1 ]"   ❌ comma eaten
//  expected: "array literal: [1, ]"

It only triggers on the strip path (a comment-bearing config — exactly the "configs we did not write" case parseJsonc exists for) and a string containing , ] or , } (command strings, regexes, prompts). Silent config corruption.

Suggested fix — make the trailing-comma removal string/comment-aware inside the scanner and drop the post-hoc regex:

export function stripJsonComments(str: string): string {
  let out = "";
  let inString = false;
  let escaped = false;
  let inBlockComment = false;
  for (let i = 0; i < str.length; i++) {
    const c = str[i];
    const next = str[i + 1];
    if (inBlockComment) {
      if (c === "*" && next === "/") { inBlockComment = false; i++; }
      continue;
    }
    if (escaped) { out += c; escaped = false; continue; }
    if (c === "\\") { out += c; escaped = inString; continue; }
    if (c === '"') { inString = !inString; out += c; continue; }
    if (!inString) {
      if (c === "/" && next === "/") {
        while (i < str.length && str[i] !== "\n") i++;
        if (i < str.length) out += "\n";
        continue;
      }
      if (c === "/" && next === "*") { inBlockComment = true; i++; continue; }
      // String-aware trailing-comma removal: a comma is "trailing" only when the
      // next significant char (skipping whitespace + comments) is } or ].
      if (c === ",") {
        let j = i + 1;
        while (j < str.length) {
          const d = str[j];
          if (d === " " || d === "\t" || d === "\r" || d === "\n") { j++; continue; }
          if (d === "/" && str[j + 1] === "/") { while (j < str.length && str[j] !== "\n") j++; continue; }
          if (d === "/" && str[j + 1] === "*") { j += 2; while (j < str.length && !(str[j] === "*" && str[j + 1] === "/")) j++; j += 2; continue; }
          break;
        }
        if (str[j] === "}" || str[j] === "]") continue; // drop the trailing comma
        out += c;
        continue;
      }
    }
    out += c;
  }
  return out; // no more post-hoc regex
}

Worth a unit test asserting parseJsonc preserves , ] / , } inside a string on the JSONC path.


🟠 3. Debug instrumentation shipped to prod — posttooluse.mjs + precompact.mjs

logDebug() writes ~/.copilot/context-mode/*-debug.log via appendFileSync on every invocation (CALL:, OK:, ERR:). That's unbounded file growth in the user's config dir on every PostToolUse. It's also inconsistent — sessionstart/stop/userpromptsubmit have no such logging — so it reads like leftover debug.

Simplest fix: gate it behind an env so it's opt-in (or drop it entirely):

const DEBUG = process.env.CONTEXT_MODE_DEBUG === "1";
function logDebug(line) {
  if (!DEBUG) return;
  try {
    mkdirSync(dirname(DEBUG_LOG), { recursive: true });
    appendFileSync(DEBUG_LOG, line);
  } catch {
    /* silent */
  }
}

Nice work overall — the marker that gates copilot-cli detection on a context-mode-written file (so it won't hijack a co-installed Claude Code user) is exactly right. Happy to re-test once the above are in. 👍

@mksglu

mksglu commented Jun 6, 2026

Copy link
Copy Markdown
Owner

@ken-jo , can we follow CONTRIBUTING.md and always update the existing files instead of creating new ones?Creating new files makes maintenance harder, so I’d prefer us to modify the current files whenever possible. cc: @ousamabenyounes

ken-jo and others added 5 commits June 7, 2026 04:35
…#787 review)

A thrown PreToolUse hook exited non-zero with empty stdout, which GitHub
Copilot CLI 1.0.59 treats as "Denied by preToolUse hook (hook errored)" and
uses to block EVERY tool — bricking the agent. parseStdin runs JSON.parse, so
a malformed payload alone triggers it. Wrap the hook body in a fail-open
try/catch: a legitimate veto is a normal stdout write + return (never a
throw), so only real errors are swallowed (empty stdout + exit 0 => ALLOW).
Adds a regression test that spawns the hook with a throwing payload.

Also gate the per-invocation debug logs (posttooluse/precompact/sessionstart)
behind CONTEXT_MODE_DEBUG, matching the kimi hooks — the PostToolUse log grew
on every tool call under the user's config dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stripJsonComments stripped trailing commas with a regex over the whole string,
silently eating commas INSIDE string values (e.g. "[1, ]" -> "[1 ]") on the
comment-strip path (reached whenever strict JSON.parse fails). Move the
trailing-comma removal into a second string-aware pass over the comment-free
output: in-string commas are preserved while real trailing commas — including
those separated from } or ] by a comment — are still stripped. Regenerated
bundles (jsonc is bundled into cli/server.bundle.mjs).

The identical duplicates in src/server.ts and src/adapters/opencode/index.ts
are left for a follow-up consolidation PR (they parse third-party configs;
wider blast radius).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…review)

CONTRIBUTING.md ("Test file organization") keeps one test file per adapter /
core module. Merge the standalone bundle-guard + schema files into their
canonical homes and delete the standalones — zero net-new test files:
  - copilot-cli-plugin.test.ts    -> adapters/copilot-cli.test.ts
  - antigravity-cli-plugin.test.ts -> adapters/antigravity.test.ts
  - strict-client-schema.test.ts  -> core/server.test.ts (sanitizeSchemaForStrictClients)

Also add the jsonc string-aware regression test to core/server.test.ts (its
home per the domain table; jsonc.ts has no test file of its own).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntion (mksglu#787 review)

The repo's per-platform hook test files are named tests/hooks/<platform>-hooks.test.ts
(cursor-hooks, gemini-hooks, vscode-hooks, jetbrains-hooks, kiro-hooks, kimi-hooks).
copilot-cli's was the lone deviation (copilot-cli-capture.test.ts). Rename it to
copilot-cli-hooks.test.ts and add the matching row to the CONTRIBUTING.md test-file
table. (antigravity-cli stays folded into antigravity.test.ts — capture-only single
hook, mirroring the GUI variant in the same family file, per the repo's precedent.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… review)

configs/copilot-cli/.github/plugin/plugin.json carries a pinned "version" but,
unlike the antigravity-cli bundle, was missing from version-sync — so it would
freeze on the next `npm version` bump (the .cursor-plugin v1.0.111 drift class
the version-sync test guards against). Add it to scripts/version-sync.mjs targets,
the package.json `version` git-add list, and the version-sync test (targets + pkg
list + SHIPPED lockstep + end-to-end), mirroring the agy bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ken-jo

ken-jo commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ousamabenyounes and @mksglu — all three review items + the CONTRIBUTING guidance are addressed. Pushed as 5 focused commits; full suite green (185 files, 4265 passed / 44 skipped, 0 failed) and the bundles are regenerated in lockstep.

Reviewer items

  • 🟠 Demo video #1 PreToolUse brick — wrapped hooks/copilot-cli/pretooluse.mjs in a fail-open try/catch. A legitimate veto is a normal stdout write + return (never a throw), so only real errors are swallowed → empty stdout + exit 0 = host ALLOWS, no brick. Added a regression test that spawns the hook with a throwing payload and asserts exit 0 + no deny.
  • 🟠 Submit to Cursor Marketplace #2 jsonc trailing-comma — made the trailing-comma strip string-aware (a second pass over the comment-free output) so commas inside string values are preserved while real trailing commas — including ones separated from }/] by a comment — are still stripped. parseJsonc still tries strict JSON.parse first. Unit test added. The identical duplicates in src/server.ts and src/adapters/opencode/index.ts are deferred to a follow-up consolidation PR (they parse third-party configs — wider blast radius).
  • 🟠 feat: add Cursor Marketplace support #3 debug logs — gated the per-invocation debug writes behind CONTEXT_MODE_DEBUG (posttooluse + precompact + sessionstart), matching the kimi-hook precedent; the stdout/exit contract is unchanged.

CONTRIBUTING (@mksglu) — consolidated to update existing files instead of adding new ones:

  • Merged the 3 standalone test files into their canonical homes and deleted them: copilot-cli-plugintests/adapters/copilot-cli.test.ts, antigravity-cli-plugintests/adapters/antigravity.test.ts, strict-client-schematests/core/server.test.ts. Net-new test files at PR scope are now just 2 — tests/adapters/copilot-cli.test.ts (new adapter) and tests/hooks/copilot-cli-hooks.test.ts (new-platform hooks) — both required by the per-platform convention; antigravity-cli reuses the existing antigravity family file.
  • Renamed copilot-cli-capture.test.tscopilot-cli-hooks.test.ts to match the <platform>-hooks.test.ts convention (cursor/gemini/vscode/jetbrains/kiro/kimi) and added the row to the CONTRIBUTING test-file table.
  • Caught + fixed a drift bug while auditing: the Copilot bundle manifest (configs/copilot-cli/.github/plugin/plugin.json) had a pinned version but wasn't in version-sync (the agy bundle was) — it would have frozen on the next bump. Registered it in version-sync.mjs, the package.json version git-add list, and the version-sync test.

Follow-ups (out of scope here, will send separately): collapse the 3 stripJsonComments copies into the shared util, and optional dispatcher-level (src/cli.ts) fail-open so every platform's PreToolUse hook shares the brick guard. Happy to re-test.

@@ -0,0 +1,15 @@
{
"hooks": {
"PostToolUse": [

@mksglu mksglu Jun 7, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need at least preToolUse hook in here. @ken-jo

ken-jo and others added 3 commits June 7, 2026 18:38
…e decision contract

agy honors a top-level PreToolUse decision `{"decision":"deny"|"ask",reason}`
(verified on agy 1.0.6) — not Claude's permissionDecision/additionalContext — so
context-mode can ENFORCE routing on agy, not just capture.

- PreToolUse routing hook (hooks/antigravity-cli/pretooluse.mjs) emits agy's
  native decision; deny/ask enforce, context/modify collapse to an enforceable
  deny (agy ignores additionalContext). Fail-open.
- Shared agy payload mapper (hooks/antigravity-cli/payload.mjs) used by
  pre/post/stop; posttooluse refactored onto it. New capture-only Stop hook
  (best-effort — agy Stop firing unconfirmed, so it's excluded from doctor health).
- Native root bundle: ships plugin.json + mcp_config.json + hooks.json +
  rules/context-mode.md (agy reads bundle-ROOT files); .mcp.json and
  .claude-plugin/plugin.json removed. hooks/hooks.json kept as the validate/install
  mirror — agy runtime fires from root hooks.json, but `agy plugin validate/install`
  only REPORTS hooks when the subdir hooks/hooks.json also exists.
- routing.mjs agy aliases (run_command->Bash, view_file->Read, ...) + CommandLine/
  AbsolutePath/URL extractors; tool-naming.mjs maps agy to context-mode/<tool>.
- adapter: capabilities preToolUse/postToolUse true, paradigm json-stdio, native
  decision formatter, doctor; cli.ts HOOK_MAP pretooluse/posttooluse/stop;
  version-sync tracks the bundle plugin.json.

Fixes a marker-handoff bug: pretooluse keyed rejected/redirect markers on
conversationId while posttooluse reads via getSessionId (which prefers the
transcript UUID) — both now use getSessionId, with a <uuid>.jsonl round-trip
regression test. Also corrects a stale core-routing assertion to agy's
context-mode/<tool> surface, adds the CONTRIBUTING test-file row, and includes
incidental CODEX_* test-env isolation hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ack, sync comments, test placement

- formatters: agy `modify` now surfaces routing's per-tool redirect guidance
  (curl/build-tool/inline-HTTP) extracted from the echo payload instead of one
  generic line; `ask` carries a fallback reason so a security-policy confirmation
  prompt is never bare. Adapter formatPreToolUseResponse ask branch mirrored.
- comments: cross-reference the three agy tool-name maps (payload.mjs /
  routing.mjs / extract.ts) and the two agyContextReason copies (formatters.mjs /
  adapter) so they don't silently drift (single shared table = follow-up).
- tests: move the agy formatter tests to the canonical tests/hooks/formatters.test.ts
  (formatDecision wrapper style, beside the other per-platform blocks); assert the
  surfaced modify guidance + the ask fallback. Update the run_command deny test to
  the specific (non-generic) guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two agy-specific hardening fixes surfaced by interactive testing:

- ctx_execute / ctx_execute_file / ctx_batch_execute apply a default execution
  timeout (120s, tunable via CONTEXT_MODE_AGY_EXEC_TIMEOUT_MS) ONLY under agy.
  agy does not enforce an MCP RPC timeout, so a runaway/blocking script hung
  forever and had to be interrupted; every other host keeps the unbounded
  behavior (Issue mksglu#406). resolveExecTimeout() centralizes this; timed-out
  messages now report the effective timeout (was "undefinedms"). Unit-tested +
  e2e-verified (runaway ctx_execute killed at the bound instead of hanging).
- rules/context-mode.md: add a prominent "Do not dump — derive" section. agy
  artifacts each MCP tool's stdout to a step file the model then reads back, so
  a whole-file dump costs the context window twice; steer the model to
  value/match/known-slice extraction instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ken-jo

ken-jo commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@mksglu — addressed the preToolUse request, and it turned into a real upgrade: agy now has bounded PreToolUse enforcement, not just capture. (This corrects my earlier "capture-only" framing.)

What I verified on real agy 1.0.6

  • agy honors its native top-level decision contract {"decision":"deny"|"ask","reason"} for PreToolUse — not Claude's permissionDecision / hookSpecificOutput, and it ignores PreToolUse additionalContext. So the routing decisions now actually block: raw curl/wget/inline-HTTP/build-tools are denied and redirected to the context-mode/ctx_* tools, with the per-tool guidance surfaced in the deny reason.
  • Bundle migrated to the agy-native root format: ships plugin.json + mcp_config.json + hooks.json + rules/context-mode.md at the bundle root (agy reads the bundle-root files — confirmed via agy plugin validate/install). The old .mcp.json + .claude-plugin/plugin.json are removed.
  • Dual hooks.json is intentional (agy quirk): the runtime fires hooks from the root hooks.json, but agy plugin validate/install only reports hooks when the subdir hooks/hooks.json also exists — so the bundle keeps both, byte-identical.
  • Tool-name surface fixed to agy's context-mode/<tool>; routing aliases added (run_command→Bash, view_file→Read, grep_search→Grep, read_url_content→WebFetch, …) with CommandLine/AbsolutePath/URL arg extractors.
  • Stop hook is registered best-effort — agy did not fire Stop in the -p probe (interactive unconfirmed), so it's excluded from the doctor health gate.
  • agy exec-timeout hardening: agy does not enforce an MCP RPC timeout, so a runaway ctx_execute hung until interrupted. ctx_execute/ctx_execute_file/ctx_batch_execute now apply a default timeout (120s, tunable via CONTEXT_MODE_AGY_EXEC_TIMEOUT_MS) only under agy; every other host keeps the unbounded behavior (Issue [Feature]: Server-enforced timeout policy for long-running build commands in execute tools #406).
  • version-sync tracks the new bundle plugin.json; per-platform tests added (tests/hooks/antigravity-cli-hooks.test.ts, formatter/routing/tool-naming/version-sync coverage). Full suite green.

Follow-up (separate PR, out of scope here): the agy tool-name map is currently mirrored in three places (payload.mjs / routing.mjs / extract.ts); I left cross-reference comments and will collapse them into one shared table in a follow-up.

Happy to re-test on your side.

@mksglu

mksglu commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Hi @ken-jo Resolve conflicts

Only conflict was the generated hooks/session-extract.bundle.mjs; resolved by
regenerating all bundles from the merged source (npm run build, assert-bundle OK).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ken-jo ken-jo force-pushed the feat/copilot-cli-antigravity-cli branch from 7a17a31 to 9667e13 Compare June 7, 2026 23:52
GitHub Copilot CLI (verified against the @github/copilot 1.0.60 binary)
dispatches hooks by camelCase event names ONLY — preToolUse / postToolUse /
sessionStart / userPromptSubmitted / agentStop / preCompact. The adapter
shipped PascalCase keys (PreToolUse / ...), which the binary silently ignores,
so context-mode's PreToolUse routing enforcement and PostToolUse capture never
fired on Copilot CLI. MCP tool exposure (.mcp.json auto-discovery) was
unaffected, which masked the regression.

- HOOK_TYPES values -> Copilot's camelCase. UserPromptSubmit->userPromptSubmitted
  and Stop->agentStop are NAME changes, not just casing.
- Decouple the CLI dispatch token from the event name: buildHookCommand now
  derives the token from the .mjs script base (pretooluse, ...), so the event
  KEY can be camelCase while the dispatcher and cli.ts hook handler stay stable.
- Update configs/copilot-cli/hooks.json keys, README, index.ts comments, tests.

Verified e2e on real Copilot CLI 1.0.60 via the documented plugin install:
PreToolUse denied a raw `curl` and redirected to ctx_fetch_and_index (the model
obeyed); PostToolUse fired (posttooluse-debug.log advanced under
CONTEXT_MODE_DEBUG). The internal DB event-type labels in hooks/copilot-cli/*.mjs
are context-mode's cross-adapter taxonomy and are intentionally unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ken-jo ken-jo requested a review from mksglu June 8, 2026 14:11
mksglu and others added 2 commits June 8, 2026 18:21
Mac smoke testing found that Copilot CLI 1.0.44 rejects the plugin MCP entry before startup unless the no-argument server still declares an explicit empty args array.

Constraint: Copilot CLI 1.0.44 requires an explicit args array for plugin stdio MCP entries

Rejected: Omit args because context-mode takes no arguments | older Copilot CLI rejects the plugin config before MCP startup

Confidence: high

Scope-risk: narrow

Directive: Keep args: [] in the Copilot plugin .mcp.json unless Copilot documents it as optional across supported versions

Tested: vitest copilot-cli adapter and hook suites; real Copilot CLI 1.0.44 loaded context-mode MCP after patch; real agy prompt returned 12

Not-tested: Copilot prompt completion, because local Copilot CLI fails to list models even without this plugin

Co-authored-by: OmX <omx@oh-my-codex.dev>
@ken-jo

ken-jo commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

I re-tested PR #787 on macOS after the Mac issue report.

Findings and fix:

  • Reproduced the Copilot-side Mac failure with GitHub Copilot CLI 1.0.44: the plugin .mcp.json was rejected before MCP startup because the stdio entry had command but no explicit args array.
  • Pushed 94ac918, which adds "args": [] to the Copilot plugin MCP config and locks it with a regression assertion.
  • After the patch, the same Copilot 1.0.44 local plugin smoke reaches MCP client for context-mode connected. Full Copilot prompt completion is still blocked on this Mac by Failed to list models, and I confirmed that baseline failure happens even without the context-mode plugin, so it is separate from this PR.
  • agy was also checked on macOS: agy plugin validate passes, npm run install:agy reports hooks ACTIVE, and a real agy -p MCP smoke returned 12.

Local targeted tests passed, and the refreshed PR checks are green across Ubuntu/macOS/Windows.

Clean-install testing exposed that the package declared npm run install:agy but omitted the installer file from package.json files, so the installed tarball failed before agy plugin install could run.

Constraint: npm tarball contents are limited by package.json files

Rejected: Rely on repository-local installer presence | npm install -g ships only allowlisted files

Confidence: high

Scope-risk: narrow

Directive: Keep package scripts and package.json files in lockstep for shipped install commands

Tested: vitest antigravity and copilot adapter hook suites; npm pack includes scripts/install-antigravity-cli-plugin.mjs; npm uninstall -g context-mode then npm install -g tarball; npm --prefix installed package run install:agy; real agy prompt returned 12; Copilot loaded installed plugin MCP

Not-tested: Copilot prompt completion, because local Copilot CLI fails to list models after MCP startup

Co-authored-by: OmX <omx@oh-my-codex.dev>
@ken-jo

ken-jo commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Clean-install follow-up is done.

I removed the existing global install and host registration first:

  • npm uninstall -g context-mode
  • agy plugin uninstall context-mode
  • confirmed context-mode was no longer on PATH, global npm had no context-mode, agy had no imported plugin, and Copilot had no context-mode MCP registration.

That surfaced one more real packaging issue: the npm tarball shipped npm run install:agy but did not include scripts/install-antigravity-cli-plugin.mjs, so installed-package npm --prefix ... run install:agy failed with MODULE_NOT_FOUND. I pushed b6ce026 to include the installer in package.json#files and added a regression assertion.

Clean install verification after the fix:

  • npm pack contains both scripts/install-antigravity-cli-plugin.mjs and configs/copilot-cli/.mcp.json.
  • npm install -g context-mode-1.0.162.tgz succeeds from the PR tarball.
  • installed package contains Copilot .mcp.json with args: [].
  • installed package npm run install:agy succeeds and reports hooks ACTIVE.
  • real agy -p MCP smoke returned 12.
  • Copilot CLI 1.0.44 loads the installed package plugin bundle and reaches MCP client for context-mode connected.

Remote checks are green again on the new head: Ubuntu/macOS/Windows test + Ubuntu/macOS OpenClaw E2E.

Update after local network change: Copilot prompt completion is now verified too. Re-running the installed-package plugin smoke returned 72 for a 9 * 8 request, the Copilot log shows MCP client for context-mode connected, and the isolated context-mode data directory recorded one ctx_execute tool call. That makes the earlier Failed to list models result a local Copilot/model-list network issue after MCP startup, not a PR/package failure.

mksglu pushed a commit that referenced this pull request Jun 10, 2026
fix(opencode,server): string-aware JSONC stripping via shared util/jsonc (#806)

opencode.jsonc/kilo.jsonc with URLs in string values (e.g. "$schema":
"https://opencode.ai/config.json") were corrupted by the adapter's naive
stripJsonComments regex (/\/\/.*$/gm eats everything after "https:"),
so readSettings() returned null and ctx doctor emitted the false
"[FAIL] Plugin configuration: Could not read opencode.json or
opencode.jsonc".

- add src/util/jsonc.ts: two-pass string-aware comment + trailing-comma
  stripping and tolerant parseJsonc (byte-identical to the #787-reviewed
  implementation so the branches merge cleanly)
- opencode adapter: drop the naive regex stripper (introduced in 7189ed6
  to avoid a jsonc-parser dependency; the shared util keeps that
  property — zero imports, esbuild inlines it), route through the util
- server.ts: drop its local copy whose trailing-comma regex corrupted
  commas inside string values; import the shared util
- tests: URL-bearing opencode.jsonc/kilo.jsonc through the public
  adapter API, in-string comma regression, escaped quotes, CRLF, block
  comments with URLs, trailing commas, plain-JSON passthrough

Verified end-to-end on real OpenCode 1.17.1: URL-bearing opencode.jsonc
parses, doctor reports PASS, plugin loads and captures session events.

Closes #806

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@ken-jo

ken-jo commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer-style validation update

@mksglu I validated this against the maintainer review requirements and updated the branch accordingly.

Why this PR is needed

This PR resolves #774 and #775.

The visible product gaps were:

  • Antigravity CLI / agy did not have a reliable first-class Context Mode integration path.
  • GitHub Copilot CLI needed official MCP + hook support documentation and runtime wiring.
  • The previous attempt, Add Copilot CLI and Antigravity CLI support #779, did not work end-to-end: agy could be misdetected as Claude Code and the agy MCP tools did not reliably reach the model.

This PR is the narrower replacement for that earlier attempt. It focuses on the two CLI agents and the cross-cutting runtime fixes they actually need.

Git archaeology / prior invariants checked

I checked the related history before treating this as mergeable:

The source conflict in src/server.ts had to preserve both sides: this PR's Antigravity timeout behavior and upstream #765's cwd passthrough behavior. The resolved code keeps both.

Problems fixed during the dirty update

I updated the branch onto latest next, resolved the src/server.ts conflict, and rebuilt generated bundles from source.

The visible conflict risk was that resolving src/server.ts one-sidedly could either:

  • keep Antigravity's timeout normalization but drop upstream cwd passthrough, or
  • keep upstream cwd passthrough but drop Antigravity's timeout normalization.

The final resolution preserves both invariants in the same executor call.

I also fixed docs drift discovered during validation. Runtime config in configs/copilot-cli/hooks.json uses camelCase hook keys, but README/platform docs still described the Copilot CLI hook shape as PascalCase / VS Code-family style. That stale wording mattered because 778c3dd showed Copilot CLI silently ignores PascalCase hook keys. The docs now match the actual runtime config.

The same fresh-checkout test-harness bug applied here too:

npx tsc --silent

TypeScript rejects that option, so I changed it to:

npx tsc --pretty false

This keeps the test's non-interactive compile purpose while making fresh worktree validation pass.

Validation

Fresh branch head: eb41f45

Environment Command / check Result
Linux pnpm run build passed
Linux bundle assertions / asymmetric drift checks passed
Linux Copilot CLI hooks/adapters, Antigravity hooks/adapters, detection/config-dir targeted tests passed
Linux targeted server coverage for IDEA_INITIAL_DIRECTORY, resolveExecTimeout, and strict-client schema sanitization passed
macOS fresh worktree hooks/adapters suite 194 passed
macOS fresh worktree targeted server tests: IDEA_INITIAL_DIRECTORY, resolveExecTimeout, sanitizeSchemaForStrictClients passed
Windows fresh worktree hooks/adapters suite 194 passed
Windows fresh worktree targeted server tests: IDEA_INITIAL_DIRECTORY, resolveExecTimeout, sanitizeSchemaForStrictClients passed
GitHub Actions CI test on ubuntu, macOS, Windows passed
GitHub Actions OpenClaw E2E on ubuntu/macOS passed

Conclusion

I consider this merge-ready from the branch/validation side.

The update preserves the Antigravity timeout behavior, preserves upstream #765 cwd passthrough, and fixes the Copilot CLI docs/runtime mismatch so users are not directed toward hook keys that the CLI silently ignores.

@ken-jo

ken-jo commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Additional latest CLI retest

@mksglu I ran an additional latest-version CLI check for the two new CLI agents in this PR.

Versions tested

  • GitHub Copilot CLI: 1.0.62
  • Antigravity CLI / agy: 1.0.8
  • Branch under test: eb41f45

For the live MCP checks, I put a local context-mode wrapper first on PATH so the CLIs invoked this PR's cli.bundle.mjs, not any globally installed context-mode package.

Results

Environment CLI Check Result
Linux Copilot CLI 1.0.62 --plugin-dir configs/copilot-cli, live ctx_execute call through this PR's MCP server passed, returned 17 for 8 + 9
Linux agy 1.0.8 npm run install:agy, then live ctx_execute call through this PR's MCP server passed, returned 13 for 9 + 4
macOS Copilot CLI 1.0.62 CLI version/update check installed, but live prompt was not counted as validation because the macOS GitHub/Copilot auth is invalid (gh auth status reports the token is invalid; Copilot fails model listing)
macOS agy 1.0.8 npm run install:agy plugin install succeeded: MCP server + routing rule + routing skill + hooks registered, stale schema cache cleared
macOS agy 1.0.8 live prompt / ctx_execute not counted as validation because the CLI requires sign-in on that host
Windows Copilot CLI 1.0.62 --plugin-dir configs/copilot-cli, live ctx_execute call through this PR's MCP server passed, returned 16 for 11 + 5
Windows agy 1.0.8 npm run install:agy plugin install succeeded: MCP server + routing rule + routing skill + hooks registered, stale schema cache cleared
Windows agy 1.0.8 live prompt / ctx_execute not counted as validation: agy -p over SSH produced no stdout even for a simple Answer only: ok smoke prompt; direct cmd.exe execution hung until stopped. The log showed auth recovery and hook execution, so I am treating this as an agy 1.0.8/SSH print-mode validation blocker rather than a passing PR signal.

Interpretation

The latest Copilot CLI path still looks good on Linux and Windows with the local plugin bundle and this PR's MCP server.

The latest agy path is confirmed on Linux, and the installer path works on macOS and Windows. I am not claiming macOS/Windows live agy prompt success because those hosts did not produce reliable model-output evidence.

@mksglu

mksglu commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Hi @ken-jo Please resolve conflicts

@mksglu mksglu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conflicts

# Conflicts:
#	cli.bundle.mjs
#	server.bundle.mjs
#	tests/integration/project-dir-strict.test.ts
@ken-jo

ken-jo commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@mksglu done

@ken-jo

ken-jo commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@mksglu I revalidated the latest #787 update against the maintainer review requirements.

Why this update is needed:

  • Git history shows Copilot CLI support was introduced in this PR and then expanded from a manual COPILOT_HOME style setup into a real Copilot CLI plugin bundle.
  • That changes the runtime invariant: when Copilot loads context-mode through the plugin bundle, MCP and hook validation must be evaluated against the plugin-scoped bundle, not only against a user-managed Copilot home config.
  • The latest update makes that distinction explicit with CONTEXT_MODE_COPILOT_PLUGIN=1, validates the Copilot plugin bundle hooks from configs/copilot-cli/hooks.json, checks all six Copilot hook entries, and avoids reporting the plugin/manual setup through the misleading configured-version path.
  • I also verified detection ordering: an explicit COPILOT_HOME with a valid Copilot config still wins over passive Antigravity markers, so this does not route Copilot sessions into the agy adapter.

Validation performed:

  • git diff --check passed.
  • npx vitest run tests/adapters/copilot-cli.test.ts tests/adapters/detect-config-dir.test.ts --reporter=dot passed.
  • The broader related adapter/hook validation suite passed.
  • npm run typecheck passed.
  • npm run build passed.
  • npm pack --pack-destination /tmp succeeded.
  • Generated bundles were refreshed and included: cli.bundle.mjs, server.bundle.mjs, and hooks/security.bundle.mjs.

Real Agent / CLI validation:

  • Linux:

    • Copilot CLI 1.0.63: plugin runtime doctor passed, manual config doctor passed, explicit COPILOT_HOME detection passed, and a real Copilot agent invocation successfully called a context-mode MCP tool.
    • Antigravity CLI agy 1.0.9: doctor passed, and a real agy agent invocation successfully called ctx_execute.
  • Windows:

    • Windows NT 10.0.26200, Node 24.13.0, npm 11.6.2.
    • Copilot CLI 1.0.63: plugin runtime doctor passed, manual config doctor passed, explicit COPILOT_HOME detection passed, and a real Copilot agent invocation successfully called a context-mode MCP tool.
    • Antigravity CLI agy 1.0.9: doctor passed, and the verification store recorded the real ctx_execute MCP tool call from agy.
  • macOS:

    • Darwin 25.2.0 arm64, Node 26.0.0, npm 11.12.1.
    • Copilot CLI 1.0.63: direct plugin/manual doctor gates passed. With the system HTTPS proxy exported into the CLI environment, Copilot successfully loaded the model and the debug log confirmed a real context-mode-ctx_execute MCP tool_use.
    • Antigravity CLI agy 1.0.9: doctor passed, and the verification store recorded the real ctx_execute MCP tool call from agy.

Conclusion:

This update is meaningful and merge-worthy from my validation side. It preserves the existing Copilot CLI support while fixing the plugin-runtime validation path, and the affected Copilot/agy behavior was verified through real agent invocations across Linux, Windows, and macOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants