agy-plugin-cc 0.6.1 → 0.6.4

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
@@ -23,6 +23,7 @@ This plugin is for Claude Code users who want an easy way to start using Antigra
23
23
  - **Google account for Antigravity sign-in.**
24
24
  - Delegations contribute to your Antigravity usage limits. Check them anytime with `/agy:quota`.
25
25
  - **Node.js 18 or later**
26
+ - **git**, for the review commands. The companion script collects the diff itself, so a missing git is reported as `git is not installed or not on PATH` rather than an empty review.
26
27
 
27
28
  ## Install
28
29
 
@@ -68,10 +69,12 @@ On Windows (PowerShell):
68
69
  irm https://antigravity.google/cli/install.ps1 | iex
69
70
  ```
70
71
 
72
+ The plugin's own test suite runs on Windows as well as Linux in CI, including a check that it can actually execute the `.cmd` shims npm installs there.
73
+
71
74
  If agy is installed but not signed in yet, run it once interactively:
72
75
 
73
76
  ```bash
74
- !agy
77
+ ! agy
75
78
  ```
76
79
 
77
80
  After install, you should see:
@@ -159,6 +162,7 @@ Examples:
159
162
  **Notes:**
160
163
 
161
164
  - if you do not pass `--model` or `--effort`, agy chooses its own defaults
165
+ - some models reject `--effort` outright (agy 1.2.4 refuses it for `claude-opus-4-6-thinking` before running anything); the rescue agent then reruns once without the flag and says so
162
166
  - write-capable runs use `--mode accept-edits` on the agy side; review and diagnosis runs stay read-only
163
167
  - follow-up rescue requests can continue the latest agy conversation
164
168
 
@@ -200,7 +204,7 @@ Afterwards, continue in a terminal with `agy --conversation <id>`, or from Claud
200
204
 
201
205
  ### `/agy:status`
202
206
 
203
- Shows active and recent background agy delegations in this session.
207
+ Shows active and recent background agy work in this session. Two kinds appear, and both are listed: `agy:agy-rescue` subagent delegations from `/agy:rescue --background`, and backgrounded companion runs from `/agy:adversarial-review --background`, which are Claude Code background tasks rather than subagents.
204
208
 
205
209
  Examples:
206
210
 
@@ -217,7 +221,7 @@ Use it to:
217
221
 
218
222
  ### `/agy:result`
219
223
 
220
- Shows the stored final output of a finished background delegation, including the `conversation_id` so you can reopen that run with `/agy:continue` or `agy --conversation <id>`.
224
+ Shows the stored final output of a finished background run, subagent delegation or companion review alike, including the `conversation_id` so you can reopen that run with `/agy:continue` or `agy --conversation <id>`.
221
225
 
222
226
  Examples:
223
227
 
@@ -239,7 +243,11 @@ Examples:
239
243
 
240
244
  ### `/agy:setup`
241
245
 
242
- Checks agy readiness through a companion script (`scripts/agy-setup.mjs`) that runs every check itself (agy on PATH, auth probe, tool-exercising probe for headless permission denial, stop-review gate state) and prints a single JSON report with `ready`, per-check sections, and `nextSteps`.
246
+ Checks agy readiness through a companion script (`scripts/agy-setup.mjs`) that runs every check itself (agy on PATH, auth probe, a command probe and a file-read probe for headless permission denial, stop-review gate state) and prints a single JSON report with `ready`, per-check sections, and `nextSteps`.
247
+
248
+ Headless agy auto-denies any tool its `permissions.allow` rules do not cover, and it reports the refusal as `denied_actions` on an otherwise successful-looking result. The two probes exercise a terminal command and a file read, because the two need different rules (`command(...)` and `read_file(*)`) and `--mode accept-edits` covers neither. The report names each denied tool and the rule for it. That settings edit is yours to make by hand, in your own terminal: in a Claude Code auto mode session the classifier blocks the agent from editing the file, from passing `--dangerously-skip-permissions`, and even from querying `agy -p "/permissions"`.
249
+
250
+ When a probe fails, the report says which of three things went wrong rather than assuming a login problem: a real authentication failure, a restricted environment where the invoking shell blocked a syscall agy needs, or an unknown cause. Only the first is fixed by signing in again.
243
251
 
244
252
  You can also use `/agy:setup` to manage the optional stop-review gate.
245
253
 
@@ -332,7 +340,7 @@ Either way, the result comes back with a `conversation_id`, so the thread stays
332
340
 
333
341
  Off by default. When enabled, a Stop hook runs a read-only `agy -p` review of the previous Claude turn before the session is allowed to end. The reviewer answers `ALLOW:` or `BLOCK:` on its first line; a block keeps the session open with the reviewer's reason. Turns without code changes are allowed through immediately.
334
342
 
335
- - Toggle per project with `/agy:setup gate on` / `gate off`; state lives in `.claude/agy.local.md` (`stop_review_gate: true`).
343
+ - Toggle per workspace with `/agy:setup gate on` / `gate off`. The flag is stored outside the repository, under `CLAUDE_PLUGIN_DATA` and keyed by a hash of the workspace root, so there is nothing in your project to commit by mistake. A gate enabled under the older in-repository `.claude/agy.local.md` is still honoured until you set it through the command.
336
344
  - If agy is missing, the gate skips with a note instead of blocking.
337
345
  - Review failures and timeouts block with guidance to run `/agy:review` manually or turn the gate off.
338
346
 
@@ -364,15 +372,32 @@ Job control stays thin too: `/agy:status`, `/agy:result`, and `/agy:cancel` read
364
372
  │ └── transfer.md
365
373
  ├── hooks/
366
374
  │ └── hooks.json Stop hook wiring for the stop-review gate
375
+ ├── prompts/
376
+ │ ├── adversarial-review.md challenge-review prompt
377
+ │ ├── review.md code-review prompt
378
+ │ ├── stop-review-gate.md stop-gate prompt
379
+ │ └── transfer.md session handoff prompt
367
380
  ├── schemas/
368
381
  │ └── review-output.schema.json adversarial review output shape
369
382
  ├── scripts/
383
+ │ ├── lib/
384
+ │ │ ├── agy.mjs agy invocation and result normalizing
385
+ │ │ ├── git.mjs diff collection
386
+ │ │ ├── process.mjs spawn without a shell, Windows shims
387
+ │ │ ├── prompts.mjs prompt loader
388
+ │ │ ├── state.mjs per-workspace state
389
+ │ │ ├── stop-review.mjs stop-review gate decision logic
390
+ │ │ └── workspace.mjs repository root resolution
391
+ │ ├── agy-companion.mjs review, transfer, quota, gate subcommands
370
392
  │ ├── agy-setup.mjs /agy:setup readiness report
393
+ │ ├── bump-version.mjs version metadata check and bump
371
394
  │ ├── npx-install.mjs npx agy-plugin-cc installer
395
+ │ ├── prepack-check.mjs refuses to npm pack with .bak files present
372
396
  │ └── stop-review-gate-hook.mjs stop-review gate (dependency-free node)
373
397
  ├── skills/
374
398
  │ ├── agy-cli-runtime/ CLI call contract
375
399
  │ └── agy-result-handling/ output presentation rules
400
+ ├── tests/ node --test suite, run with npm test
376
401
  ├── CHANGELOG.md
377
402
  ├── LICENSE
378
403
  ├── README.md
@@ -385,7 +410,7 @@ Job control stays thin too: `/agy:status`, `/agy:result`, and `/agy:cancel` read
385
410
 
386
411
  If you are already signed into agy on this machine, that account works immediately here too. This plugin uses your local Antigravity CLI authentication.
387
412
 
388
- If you have not used Antigravity yet, [install the CLI](#install) and run `!agy` once to sign in with your Google account, then run [`/agy:setup`](#agysetup) to confirm everything is ready.
413
+ If you have not used Antigravity yet, [install the CLI](#install) and run `! agy` once to sign in with your Google account, then run [`/agy:setup`](#agysetup) to confirm everything is ready. Interactive agy needs a TTY, so the `! agy` form only works where the host gives the command a real terminal; otherwise sign in from a terminal of your own.
389
414
 
390
415
  ### Does the plugin use a separate Antigravity runtime?
391
416
 
@@ -393,7 +418,7 @@ No. Every delegation is a call to the same local `agy` binary you would use dire
393
418
 
394
419
  ### Will it use the same agy settings I already have?
395
420
 
396
- Yes. The plugin picks up your existing configuration in `~/.gemini/antigravity-cli/settings.json`, including `permissions.allow` rules. Headless runs auto-deny tools not covered by those rules; [`/agy:setup`](#agysetup) detects this and shows the fix.
421
+ Yes. The plugin picks up your existing configuration in `~/.gemini/antigravity-cli/settings.json`, including `permissions.allow` rules. Headless runs auto-deny tools not covered by those rules, file reads (`read_file(*)`) as well as commands (`command(*)` or a narrower target); [`/agy:setup`](#agysetup) detects both and shows the rule to add. You make that edit by hand, outside the agent session: an auto mode session cannot apply it for you.
397
422
 
398
423
  ### Does it spend my Antigravity quota?
399
424
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "agy-plugin-cc",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "description": "Installer for the agy Claude Code plugin: use Google's Antigravity CLI (agy) from Claude Code to delegate tasks or review code.",
5
5
  "bin": {
6
6
  "agy-plugin-cc": "scripts/npx-install.mjs"
7
7
  },
8
8
  "files": [
9
9
  "scripts/npx-install.mjs",
10
+ "scripts/lib/process.mjs",
10
11
  "README.md",
11
12
  "LICENSE"
12
13
  ],
@@ -24,5 +25,10 @@
24
25
  "agy"
25
26
  ],
26
27
  "author": "toaster",
27
- "license": "MIT"
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "test": "node --test",
31
+ "check-version": "node scripts/bump-version.mjs --check",
32
+ "prepack": "node scripts/prepack-check.mjs"
33
+ }
28
34
  }
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
7
+
8
+ // npm installs `agy` and `claude` on Windows as `.cmd` and `.ps1` shims, which
9
+ // `spawnSync("agy", ...)` cannot execute: there is no `agy` file to exec.
10
+ //
11
+ // `shell: true` would fix it and must not be used. These commands are handed
12
+ // prompt text, diffs, and file paths; routing any of that through a shell turns
13
+ // argument data into shell syntax. Resolving the real file and spawning it
14
+ // directly keeps arguments as arguments.
15
+ //
16
+ // This also replaces the `which` call the setup script used, which only existed
17
+ // on Unix in the first place.
18
+ export function resolveCommand(name, env = process.env) {
19
+ const raw = String(name ?? "");
20
+ if (!raw) {
21
+ return null;
22
+ }
23
+
24
+ const isWindows = process.platform === "win32";
25
+ const extensions = isWindows
26
+ ? ["", ...String(env.PATHEXT || DEFAULT_PATHEXT).split(";").filter(Boolean)]
27
+ : [""];
28
+
29
+ const exists = (candidate) => {
30
+ try {
31
+ return fs.statSync(candidate).isFile();
32
+ } catch {
33
+ return false;
34
+ }
35
+ };
36
+
37
+ // An explicit path is used as given; only PATH entries get the extension walk.
38
+ if (raw.includes("/") || raw.includes("\\")) {
39
+ const absolute = path.resolve(raw);
40
+ for (const extension of extensions) {
41
+ if (exists(absolute + extension)) {
42
+ return absolute + extension;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ for (const dir of String(env.PATH || "").split(path.delimiter).filter(Boolean)) {
49
+ for (const extension of extensions) {
50
+ const candidate = path.join(dir, raw + extension);
51
+ if (exists(candidate)) {
52
+ return candidate;
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ export function commandAvailable(name, env = process.env) {
60
+ return resolveCommand(name, env) !== null;
61
+ }
62
+
63
+ const WINDOWS_BATCH = /\.(cmd|bat)$/i;
64
+
65
+ // Resolving the shim is only half the job: Windows cannot exec a .cmd or .bat
66
+ // image at all, and since the fix for CVE-2024-27980 Node refuses to try,
67
+ // throwing EINVAL. Such a file has to be handed to cmd.exe.
68
+ //
69
+ // cmd.exe is invoked explicitly rather than through `shell: true` because the
70
+ // difference is what stays fixed. Here the interpreter and the command file are
71
+ // decided by this function and only the arguments are quoted. With
72
+ // `shell: true` the whole line, command included, would be assembled from
73
+ // caller-supplied text.
74
+ export function quoteForCmd(value) {
75
+ const text = String(value ?? "");
76
+ // Windows argument rules: backslashes are literal unless they precede a
77
+ // quote, where they must be doubled.
78
+ const escaped = text.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
79
+ return `"${escaped}"`;
80
+ }
81
+
82
+ export function buildCmdInvocation(target, args, env = process.env) {
83
+ // /d skips AutoRun commands from the registry, /s fixes how the outer quotes
84
+ // around the whole command are parsed, /c runs it and exits.
85
+ return {
86
+ file: env.ComSpec || "cmd.exe",
87
+ args: ["/d", "/s", "/c", `"${[target, ...args].map(quoteForCmd).join(" ")}"`]
88
+ };
89
+ }
90
+
91
+ // Spawns without a shell, always. When the command cannot be resolved the raw
92
+ // name is passed through so the caller still gets a normal ENOENT result rather
93
+ // than a different error shape from this layer.
94
+ export function runCommand(name, args, options = {}) {
95
+ const resolved = resolveCommand(name) ?? name;
96
+
97
+ if (process.platform === "win32" && WINDOWS_BATCH.test(resolved)) {
98
+ const invocation = buildCmdInvocation(resolved, args);
99
+ return spawnSync(invocation.file, invocation.args, {
100
+ ...options,
101
+ shell: false,
102
+ // The argument vector is already quoted for cmd.exe above; letting Node
103
+ // quote it again would double the escaping.
104
+ windowsVerbatimArguments: true
105
+ });
106
+ }
107
+
108
+ return spawnSync(resolved, args, { ...options, shell: false });
109
+ }
@@ -4,14 +4,19 @@
4
4
  // `npx agy-plugin-cc` registers the marketplace and installs the plugin
5
5
  // through the user's local `claude` CLI. Safe to rerun.
6
6
 
7
- import { spawnSync } from "node:child_process";
7
+ import fs from "node:fs";
8
8
  import process from "node:process";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ import { runCommand } from "./lib/process.mjs";
9
12
 
10
13
  const MARKETPLACE = "Eakkapoom-Name/antigravity-plugin-cc";
11
14
  const PLUGIN = "agy@antigravity-plugin-cc";
12
15
 
13
16
  function run(args) {
14
- const result = spawnSync("claude", args, { encoding: "utf8" });
17
+ // Resolved through PATH and PATHEXT so the claude.cmd shim npm installs on
18
+ // Windows is found; a bare spawn of "claude" cannot execute it.
19
+ const result = runCommand("claude", args, { encoding: "utf8" });
15
20
  return {
16
21
  ok: result.status === 0,
17
22
  enoent: result.error?.code === "ENOENT",
@@ -19,8 +24,16 @@ function run(args) {
19
24
  };
20
25
  }
21
26
 
22
- function alreadyDone(output) {
23
- return /already/i.test(output);
27
+ // A rerun is a no-op, not a failure, but a bare /already/i over the combined
28
+ // stdout and stderr also swallows any genuine failure whose message happens to
29
+ // contain the word. Match the shape of a no-op instead. The two observed
30
+ // messages are `Marketplace '<name>' already on disk` and
31
+ // `Plugin "<name>" is already installed`; the rest are near neighbours.
32
+ const ALREADY_DONE =
33
+ /\balready\s+(?:on disk|installed|added|exists?|present|registered|configured|up[- ]to[- ]date)\b/i;
34
+
35
+ export function alreadyDone(output) {
36
+ return ALREADY_DONE.test(String(output ?? ""));
24
37
  }
25
38
 
26
39
  function fail(message) {
@@ -28,27 +41,44 @@ function fail(message) {
28
41
  process.exit(1);
29
42
  }
30
43
 
31
- console.log(`Adding marketplace ${MARKETPLACE} ...`);
32
- const add = run(["plugin", "marketplace", "add", MARKETPLACE]);
33
- if (add.enoent) {
34
- fail(
35
- "The `claude` CLI was not found on PATH. Install Claude Code first: https://code.claude.com/docs"
36
- );
37
- }
38
- if (!add.ok && !alreadyDone(add.output)) {
39
- fail(`Could not add the marketplace:\n${add.output}`);
40
- }
41
- console.log(add.ok ? "Marketplace added." : "Marketplace already added.");
44
+ function main() {
45
+ console.log(`Adding marketplace ${MARKETPLACE} ...`);
46
+ const add = run(["plugin", "marketplace", "add", MARKETPLACE]);
47
+ if (add.enoent) {
48
+ fail(
49
+ "The `claude` CLI was not found on PATH. Install Claude Code first: https://code.claude.com/docs"
50
+ );
51
+ }
52
+ // Kept for claude CLI releases where a duplicate add exits non-zero. On the
53
+ // version this was measured against, both duplicate cases exit 0, so this
54
+ // branch is unreachable there and the message below is what reports the
55
+ // no-op. Removing it would break older installs for no gain.
56
+ if (!add.ok && !alreadyDone(add.output)) {
57
+ fail(`Could not add the marketplace:\n${add.output}`);
58
+ }
59
+ // A rerun succeeds rather than failing, so the exit status alone does not say
60
+ // whether anything changed; the message does.
61
+ console.log(alreadyDone(add.output) ? "Marketplace already added." : "Marketplace added.");
42
62
 
43
- console.log(`Installing ${PLUGIN} ...`);
44
- const install = run(["plugin", "install", PLUGIN]);
45
- if (!install.ok && !alreadyDone(install.output)) {
46
- fail(`Could not install the plugin:\n${install.output}`);
47
- }
48
- console.log(install.ok ? "Plugin installed." : "Plugin already installed.");
63
+ console.log(`Installing ${PLUGIN} ...`);
64
+ const install = run(["plugin", "install", PLUGIN]);
65
+ if (!install.ok && !alreadyDone(install.output)) {
66
+ fail(`Could not install the plugin:\n${install.output}`);
67
+ }
68
+ console.log(alreadyDone(install.output) ? "Plugin already installed." : "Plugin installed.");
49
69
 
50
- console.log(`
70
+ console.log(`
51
71
  Done. Next steps:
52
72
  1. Start (or restart) Claude Code.
53
73
  2. Run /agy:setup to check that the Antigravity CLI (agy) is ready.
54
74
  If agy is not installed yet, see https://antigravity.google/docs/cli/install/`);
75
+ }
76
+
77
+ // Only install when invoked as a script; importing this module (for tests) must
78
+ // not shell out to the claude CLI.
79
+ if (
80
+ process.argv[1] &&
81
+ fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))
82
+ ) {
83
+ main();
84
+ }