agents-can-communicate 0.5.9 → 0.6.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 +2 -1
- package/bin/acc-antigravity-relay.mjs +7 -0
- package/bin/entrypoints/acc-antigravity-relay.mjs +116 -0
- package/bin/entrypoints/acc-bootstrap.mjs +4 -0
- package/bin/entrypoints/acc-hook.mjs +11 -7
- package/bin/entrypoints/antigravity-relay-binding.mjs +43 -0
- package/docs/ADAPTER_AUTHORING.md +33 -0
- package/docs/ARCHITECTURE.md +27 -4
- package/docs/CAPABILITIES.md +22 -16
- package/docs/CLI.md +21 -3
- package/docs/CONCEPTS.md +7 -0
- package/docs/CONFIGURATION.md +1 -0
- package/docs/GETTING_STARTED.md +10 -4
- package/docs/HOW_IT_WORKS.md +7 -1
- package/docs/TROUBLESHOOTING.md +99 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +91 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/PreInvocation-1.2.7.json +11 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/SessionStart-1.2.7.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-error-answers-1.2.7.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-live-push-1.2.7.json +31 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +217 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product-evidence.json +132 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product.json +29 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/hooks-first-trust-no-workspace-1.2.7.json +37 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/live-push-surfaces-1.2.7.json +38 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +5 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +329 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +119 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +98 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/hooks.mjs +201 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +708 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +170 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +116 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +123 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +195 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +5 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +37 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +25 -5
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +6 -2
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +118 -0
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +17 -3
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +8 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command-prefix.mjs +26 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +6 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +3 -1
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +27 -6
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +31 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +16 -3
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +70 -17
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +4 -0
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { chmod, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { bakeSkillCommand, blankJson, defaultAntigravityRelay, ownVersion, removeIfEmpty,
|
|
9
|
+
stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
|
|
10
|
+
from "@agents-can-communicate/adapter-sdk";
|
|
11
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
12
|
+
|
|
13
|
+
import { relayShimPath, runningRelays, stopRelays } from "./relays.mjs";
|
|
14
|
+
|
|
15
|
+
const run = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The top-level key this client namespaces hooks by. It is also the identity
|
|
19
|
+
* uninstall removes by, which is why it is a constant rather than a string
|
|
20
|
+
* repeated in three places.
|
|
21
|
+
*/
|
|
22
|
+
export const ACC_NAMESPACE = "acc";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The events ACC registers, out of the four that load.
|
|
26
|
+
*
|
|
27
|
+
* `PostInvocation` is left out deliberately. It fires with an envelope
|
|
28
|
+
* byte-identical to `PreInvocation`'s for the same invocation, and there is
|
|
29
|
+
* nothing ACC would do there that `PreInvocation` has not already done - so
|
|
30
|
+
* registering it would double this client's hook cost for no behaviour.
|
|
31
|
+
*/
|
|
32
|
+
export const ACC_REGISTERED_EVENTS = Object.freeze(["SessionStart", "PreInvocation", "Stop"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The plugin that carries ACC's skill for this client, and the id the skill
|
|
36
|
+
* then has.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately not `agents-can-communicate`. On first authenticated run this
|
|
39
|
+
* client copies ACC's Gemini CLI extension into its own plugin directory under
|
|
40
|
+
* that name, and a plugin installed under the same name is silently shadowed
|
|
41
|
+
* by the copy: /skills listed only the imported one
|
|
42
|
+
* (`fixtures/plugin-name-collision-1.2.7.json`). The copy is not ACC's to
|
|
43
|
+
* rely on - every command in it points at the Gemini CLI extension's shim, so
|
|
44
|
+
* it works exactly as long as Gemini CLI is wired and not a moment longer.
|
|
45
|
+
*/
|
|
46
|
+
export const ACC_PLUGIN_NAME = "acc";
|
|
47
|
+
export const ACC_SKILL_ID = `${ACC_PLUGIN_NAME}:acc`;
|
|
48
|
+
const IMPORTED_COPY_SKILL_ID = "agents-can-communicate:acc";
|
|
49
|
+
|
|
50
|
+
const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where a registration can go, and which one is used when nobody says.
|
|
54
|
+
*
|
|
55
|
+
* Global, decided in issue #178. It always loads, and it is where ACC already
|
|
56
|
+
* registers for every other client that keeps hooks in the user's home. The
|
|
57
|
+
* alternative is scoped to one project and does nothing at all unless that
|
|
58
|
+
* project is an open Antigravity workspace - and a default that silently
|
|
59
|
+
* registers nothing is worse than one that is broader than a given project
|
|
60
|
+
* needs. `ACC_ANTIGRAVITY_HOOKS=workspace` selects the other one.
|
|
61
|
+
*
|
|
62
|
+
* A value that is neither is refused rather than replaced by this default. On
|
|
63
|
+
* this client, quietly answering a different question from the one that was
|
|
64
|
+
* asked is the failure mode every other note in this package is about.
|
|
65
|
+
*/
|
|
66
|
+
export const HOOK_LOCATIONS = Object.freeze(["global", "workspace"]);
|
|
67
|
+
export const DEFAULT_HOOK_LOCATION = "global";
|
|
68
|
+
|
|
69
|
+
const locationOf = context => context?.antigravityHookLocation ?? DEFAULT_HOOK_LOCATION;
|
|
70
|
+
|
|
71
|
+
export const globalHooksPath = home => path.join(home, ".gemini", "config", "hooks.json");
|
|
72
|
+
export const workspaceHooksPath = workspace => path.join(workspace, ".agents", "hooks.json");
|
|
73
|
+
|
|
74
|
+
// Where this client keeps the plugins it imports by itself, including the copy
|
|
75
|
+
// of ACC's Gemini CLI extension it makes without registering any of it.
|
|
76
|
+
const importedPluginPath = home => path.join(home, ".gemini", "antigravity-cli", "plugins",
|
|
77
|
+
"agents-can-communicate");
|
|
78
|
+
const shimDir = home => path.join(home, ".gemini", "config", "acc");
|
|
79
|
+
// Where `agy plugin install` copies a plugin, and the manifest it records the
|
|
80
|
+
// install in. Both belong to the client; ACC reaches them only through `agy`.
|
|
81
|
+
export const pluginInstallPath = home => path.join(home, ".gemini", "config", "plugins",
|
|
82
|
+
ACC_PLUGIN_NAME);
|
|
83
|
+
export const vendorManifestPath = home => path.join(home, ".gemini", "config",
|
|
84
|
+
"import_manifest.json");
|
|
85
|
+
/**
|
|
86
|
+
* Where "ACC created this file" is recorded.
|
|
87
|
+
*
|
|
88
|
+
* In ACC's own data home, not in the client's tree and not in the shim
|
|
89
|
+
* directory. Two reasons, one of which cost a real uninstall:
|
|
90
|
+
*
|
|
91
|
+
* - This client reads every top-level key of `hooks.json` as an integration
|
|
92
|
+
* namespace, and one it cannot read drops the whole file. A marker inside the
|
|
93
|
+
* file registers nothing at all, valid namespaces included
|
|
94
|
+
* (`fixtures/hooks-readback-foreign-key-1.2.7.json`).
|
|
95
|
+
* - The installer removes every recorded artifact *before* it calls
|
|
96
|
+
* `uninstall`, and the shim directory is a recorded artifact. A marker kept
|
|
97
|
+
* there is already gone by the time uninstall reads it, so a `hooks.json` ACC
|
|
98
|
+
* created is judged to be the user's and left behind. Observed on a real
|
|
99
|
+
* machine: `acc uninstall` reported success and left `{}` in
|
|
100
|
+
* `~/.gemini/config/hooks.json`.
|
|
101
|
+
*
|
|
102
|
+
* One marker per registration location, so a global and a workspace install on
|
|
103
|
+
* the same machine cannot claim each other's file. The shim directory remains
|
|
104
|
+
* the fallback for a direct adapter caller with no data home, which is how the
|
|
105
|
+
* adapter is used outside the installer.
|
|
106
|
+
*/
|
|
107
|
+
const createdMarkerDir = ({ dataHome, home }) => typeof dataHome === "string" && dataHome !== ""
|
|
108
|
+
? path.join(dataHome, "acc", "adapter-antigravity")
|
|
109
|
+
: shimDir(home);
|
|
110
|
+
const createdMarker = (context, file) => path.join(createdMarkerDir(context),
|
|
111
|
+
`created-${Buffer.from(file).toString("base64url")}`);
|
|
112
|
+
|
|
113
|
+
function usage(message, details = {}) {
|
|
114
|
+
throw new AccError(EXIT.USAGE, message, details);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the one file this install owns.
|
|
119
|
+
*
|
|
120
|
+
* Both locations are implemented and both are tested. Which one is used is an
|
|
121
|
+
* input, never an assumption.
|
|
122
|
+
*/
|
|
123
|
+
export function hooksPathFor(context) {
|
|
124
|
+
const { home, antigravityWorkspace } = context;
|
|
125
|
+
const location = locationOf(context);
|
|
126
|
+
if (!HOOK_LOCATIONS.includes(location)) {
|
|
127
|
+
usage(`${JSON.stringify(location)} is not an Antigravity hook location: use global `
|
|
128
|
+
+ `(${globalHooksPath(String(home))}, always loads, applies to every session on this `
|
|
129
|
+
+ "machine) or workspace (<project>/.agents/hooks.json, scoped to one project and "
|
|
130
|
+
+ "inert unless that project is an open Antigravity workspace)",
|
|
131
|
+
{ location: location ?? null, known: [...HOOK_LOCATIONS] });
|
|
132
|
+
}
|
|
133
|
+
if (location === "global") return globalHooksPath(home);
|
|
134
|
+
if (typeof antigravityWorkspace !== "string" || antigravityWorkspace === "") {
|
|
135
|
+
usage("a workspace registration needs the project directory to write it into",
|
|
136
|
+
{ location });
|
|
137
|
+
}
|
|
138
|
+
return workspaceHooksPath(antigravityWorkspace);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read a client's own JSON, and say which file when it will not parse.
|
|
143
|
+
*
|
|
144
|
+
* Two files can be called `hooks.json` on this machine and one of them belongs
|
|
145
|
+
* to a different client entirely, so the path goes in the message.
|
|
146
|
+
*/
|
|
147
|
+
async function readJson(file, fallback) {
|
|
148
|
+
let source;
|
|
149
|
+
try {
|
|
150
|
+
source = await readFile(file, "utf8");
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.code === "ENOENT") return fallback;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(source);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new AccError(EXIT.DATA, `${file} is not valid JSON: ${error.message}`,
|
|
159
|
+
{ file, cause: error.message });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const exists = async target => {
|
|
164
|
+
try {
|
|
165
|
+
await stat(target);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The config this client loads.
|
|
174
|
+
*
|
|
175
|
+
* Namespaced by integration, then by event, and each action is flat. No
|
|
176
|
+
* `matcher`, no nested `hooks` array, no top-level `hooks` wrapper: all three
|
|
177
|
+
* are the Gemini CLI shape, all three parse, and all three register nothing.
|
|
178
|
+
*
|
|
179
|
+
* Each command carries its own event name, because the payload does not. The
|
|
180
|
+
* same shim answers every event and is told which one it is running as.
|
|
181
|
+
*/
|
|
182
|
+
export function accHookConfig(shim) {
|
|
183
|
+
return { [ACC_NAMESPACE]: Object.fromEntries(ACC_REGISTERED_EVENTS.map(event =>
|
|
184
|
+
[event, [{ type: "command", command: `sh "${shim}" ${event}`, timeout: 10 }]])) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The events ACC actually has registered, according to the client.
|
|
189
|
+
*
|
|
190
|
+
* This reads the effective hook list - what `agy -p "/hooks"` answers - and
|
|
191
|
+
* never the file that was written. The two disagree in at least four captured
|
|
192
|
+
* ways: the Gemini shape loads nothing, an unsupported event name is dropped
|
|
193
|
+
* out of an otherwise valid namespace, 1.2.4 fixed the whole file being dropped
|
|
194
|
+
* under customization token budget truncation, and 1.1.1 fixed a workspace file
|
|
195
|
+
* not loading after a folder was trusted. Every one of them looks identical on
|
|
196
|
+
* disk to a registration that works.
|
|
197
|
+
*/
|
|
198
|
+
export function registeredEvents(readback) {
|
|
199
|
+
const hooks = Array.isArray(readback?.hooks) ? readback.hooks : [];
|
|
200
|
+
const ours = hooks.find(entry => entry?.name === ACC_NAMESPACE && entry.enabled === true);
|
|
201
|
+
const actions = Array.isArray(ours?.actions) ? ours.actions : [];
|
|
202
|
+
const events = actions.map(action => action?.event)
|
|
203
|
+
.filter(event => ACC_REGISTERED_EVENTS.includes(event));
|
|
204
|
+
// In the order ACC registers them, so a caller can compare and subtract
|
|
205
|
+
// without sorting two lists that mean the same thing.
|
|
206
|
+
return ACC_REGISTERED_EVENTS.filter(event => events.includes(event));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Which file the live registration was actually loaded from. */
|
|
210
|
+
export function registeredSource(readback) {
|
|
211
|
+
const hooks = Array.isArray(readback?.hooks) ? readback.hooks : [];
|
|
212
|
+
return hooks.find(entry => entry?.name === ACC_NAMESPACE)?.source ?? null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Run the client's own binary, against the home ACC is working in.
|
|
217
|
+
*
|
|
218
|
+
* `HOME` is set explicitly. The binary honours it, and every answer it gives -
|
|
219
|
+
* what is registered, which skills load, where a plugin is installed - is an
|
|
220
|
+
* answer about that home. Inheriting the caller's would verify the machine the
|
|
221
|
+
* operator is sitting at rather than the one `acc install --home` wrote to.
|
|
222
|
+
*/
|
|
223
|
+
async function runAgy(args, { home, env, timeoutMs = 60_000 } = {}) {
|
|
224
|
+
return run("agy", args, { timeout: timeoutMs,
|
|
225
|
+
env: { ...process.env, ...env, ...(typeof home === "string" ? { HOME: home } : {}) } });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const agyFor = context => context.runAgy ?? runAgy;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A print-mode slash command's data, or a reason it has none.
|
|
232
|
+
*
|
|
233
|
+
* These answer without a turn: changelog 1.1.12 records that a print-mode slash
|
|
234
|
+
* command costs no quota and runs no model. They still need a signed-in
|
|
235
|
+
* account - captured: an unauthenticated home answers
|
|
236
|
+
* `authentication failed or timed out` - and that is said as such, because
|
|
237
|
+
* "could not ask" and "asked, and nothing is registered" are different states.
|
|
238
|
+
*/
|
|
239
|
+
async function slashCommand(context, command, extra = []) {
|
|
240
|
+
const { stdout } = await agyFor(context)(["-p", command, "--output-format", "json", ...extra],
|
|
241
|
+
{ home: context.home, env: context.env });
|
|
242
|
+
const parsed = JSON.parse(stdout);
|
|
243
|
+
const data = parsed?.command?.data;
|
|
244
|
+
if (data === undefined || data === null) {
|
|
245
|
+
const reason = typeof parsed?.error === "string" && /auth/i.test(parsed.error)
|
|
246
|
+
? "this home has no signed-in Antigravity account"
|
|
247
|
+
: `agy did not answer ${command}`;
|
|
248
|
+
throw new AccError(EXIT.DATA, reason, { command, received: Object.keys(parsed ?? {}) });
|
|
249
|
+
}
|
|
250
|
+
return data;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Ask the client what it has loaded.
|
|
255
|
+
*
|
|
256
|
+
* A workspace file is only read when its directory is an open workspace, and in
|
|
257
|
+
* print mode that means passing it with `--add-dir`.
|
|
258
|
+
*/
|
|
259
|
+
export async function probeInstalledHooks(context = {}) {
|
|
260
|
+
const { antigravityWorkspace } = context;
|
|
261
|
+
return slashCommand(context, "/hooks",
|
|
262
|
+
locationOf(context) === "workspace" && typeof antigravityWorkspace === "string"
|
|
263
|
+
? ["--add-dir", antigravityWorkspace] : []);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export const probeInstalledSkills = context => slashCommand(context, "/skills");
|
|
267
|
+
|
|
268
|
+
const probeFor = context => context.probeHooks ?? probeInstalledHooks;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* ACC's own skill, if the client loaded it from where ACC installed it.
|
|
272
|
+
*
|
|
273
|
+
* The id alone is not enough: a same-named skill from somewhere else would
|
|
274
|
+
* answer to it. The path is what says it is the copy this install put there.
|
|
275
|
+
*/
|
|
276
|
+
export function registeredSkill(readback, home) {
|
|
277
|
+
const skills = Array.isArray(readback?.skills) ? readback.skills : [];
|
|
278
|
+
return skills.find(skill => skill?.name === ACC_SKILL_ID && skill.model_invocable !== false
|
|
279
|
+
&& typeof skill.path === "string"
|
|
280
|
+
&& !path.relative(pluginInstallPath(home), skill.path).startsWith("..")) ?? null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const importedCopy = readback => (Array.isArray(readback?.skills) ? readback.skills : [])
|
|
284
|
+
.find(skill => skill?.name === IMPORTED_COPY_SKILL_ID) ?? null;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Write the registration, then make the client prove it.
|
|
288
|
+
*
|
|
289
|
+
* The write is never the answer. This client accepts a config it will not load
|
|
290
|
+
* and reports nothing, so the install is finished by asking what actually
|
|
291
|
+
* loaded and refusing when the two disagree. The refusal throws, because that
|
|
292
|
+
* is the only thing `acc install` reports as a failure; a returned `ok: false`
|
|
293
|
+
* would be recorded as a successful install.
|
|
294
|
+
*/
|
|
295
|
+
/**
|
|
296
|
+
* Give this client ACC's skill, through the client's own plugin manager.
|
|
297
|
+
*
|
|
298
|
+
* Staged in a temporary directory rather than anywhere this client discovers
|
|
299
|
+
* customizations, so no half-built copy is ever loaded, then handed to
|
|
300
|
+
* `agy plugin install`, which copies the whole directory to
|
|
301
|
+
* `~/.gemini/config/plugins/acc/` and records it in its manifest
|
|
302
|
+
* (`fixtures/plugin-install-lifecycle-1.2.7.json`). Installing again
|
|
303
|
+
* overwrites, which is what makes this idempotent.
|
|
304
|
+
*
|
|
305
|
+
* The command the skill teaches is ACC's own CLI shim, in ACC's shim
|
|
306
|
+
* directory, so it outlives the Gemini CLI integration this client otherwise
|
|
307
|
+
* borrows from.
|
|
308
|
+
*/
|
|
309
|
+
const shellQuote = value => `"${String(value).replace(/(["\\$`])/g, "\\$1")}"`;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The command ACC's context line and skill tell the agent to run once per
|
|
313
|
+
* conversation. It has to run in the agent's own shell - that is the only
|
|
314
|
+
* process holding the session endpoint - so it is a file the agent can name,
|
|
315
|
+
* not something ACC could start on its behalf. It never falls back to `acc`
|
|
316
|
+
* on the PATH the way the CLI shim does: `acc start` is not a command.
|
|
317
|
+
*/
|
|
318
|
+
async function writeRelayShim({ home, relay = defaultAntigravityRelay(), node = process.execPath }) {
|
|
319
|
+
const target = relayShimPath(home);
|
|
320
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
321
|
+
await writeFile(target, [
|
|
322
|
+
"#!/bin/sh",
|
|
323
|
+
"# Generated by ACC at install time. Starts live delivery for the Antigravity",
|
|
324
|
+
"# conversation whose shell runs it; prints one line and always exits 0.",
|
|
325
|
+
`ACC_NODE=${shellQuote(node)}`,
|
|
326
|
+
`ACC_RELAY=${shellQuote(relay)}`,
|
|
327
|
+
'if [ -x "$ACC_NODE" ] && [ -f "$ACC_RELAY" ]; then',
|
|
328
|
+
' exec "$ACC_NODE" "$ACC_RELAY" "$@"',
|
|
329
|
+
"fi",
|
|
330
|
+
'if [ -f "$ACC_RELAY" ] && command -v node >/dev/null 2>&1; then',
|
|
331
|
+
' exec node "$ACC_RELAY" "$@"',
|
|
332
|
+
"fi",
|
|
333
|
+
'echo "ACC: live delivery is not installed here; run acc install --adapter antigravity."',
|
|
334
|
+
"exit 0",
|
|
335
|
+
"",
|
|
336
|
+
].join("\n"));
|
|
337
|
+
await chmod(target, 0o755);
|
|
338
|
+
return target;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function installSkillPlugin(context) {
|
|
342
|
+
const { home, cli, node } = context;
|
|
343
|
+
const manifestExisted = await exists(vendorManifestPath(home));
|
|
344
|
+
const cliShim = await writeCliShim({ dir: shimDir(home), cli, node });
|
|
345
|
+
await writeRelayShim({ home, relay: context.antigravityRelay, node });
|
|
346
|
+
const stage = await mkdtemp(path.join(tmpdir(), "acc-antigravity-plugin-"));
|
|
347
|
+
try {
|
|
348
|
+
const plugin = path.join(stage, ACC_PLUGIN_NAME);
|
|
349
|
+
await cp(bundle, plugin, { recursive: true });
|
|
350
|
+
await stampPluginVersion({ file: path.join(plugin, "plugin.json"),
|
|
351
|
+
version: await ownVersion(import.meta.url), io: { readFile, writeFile } });
|
|
352
|
+
await bakeSkillCommand({ root: plugin, cliShim });
|
|
353
|
+
await agyFor(context)(["plugin", "install", plugin], { home, env: context.env });
|
|
354
|
+
} finally {
|
|
355
|
+
await rm(stage, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
// agy creates its manifest on first install and leaves it behind, holding
|
|
358
|
+
// `{"imports": null}`, on uninstall. Whether ACC's install is the reason it
|
|
359
|
+
// exists is recorded now, while that is still knowable.
|
|
360
|
+
if (!manifestExisted) {
|
|
361
|
+
await mkdir(createdMarkerDir(context), { recursive: true });
|
|
362
|
+
await writeFile(createdMarker(context, vendorManifestPath(home)),
|
|
363
|
+
`${vendorManifestPath(home)}\n`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Write the registration, then make the client prove it.
|
|
369
|
+
*
|
|
370
|
+
* The write is never the answer. This client accepts a config it will not load
|
|
371
|
+
* and reports nothing, so the install is finished by asking what actually
|
|
372
|
+
* loaded and refusing when the two disagree. The refusal throws, because that
|
|
373
|
+
* is the only thing `acc install` reports as a failure; a returned `ok: false`
|
|
374
|
+
* would be recorded as a successful install.
|
|
375
|
+
*
|
|
376
|
+
* An answer the client cannot give is not a refusal. A home with no signed-in
|
|
377
|
+
* account cannot run a print-mode slash command, and that is reported as
|
|
378
|
+
* unverified - never as registered.
|
|
379
|
+
*/
|
|
380
|
+
export async function installAntigravity(context) {
|
|
381
|
+
const { home, runner, node } = context;
|
|
382
|
+
const file = hooksPathFor(context);
|
|
383
|
+
const found = await readJson(file, null);
|
|
384
|
+
const existing = found ?? {};
|
|
385
|
+
|
|
386
|
+
const shim = await writeHookShim({ dir: shimDir(home), adapterId: "antigravity",
|
|
387
|
+
runner, node });
|
|
388
|
+
const merged = { ...existing, ...accHookConfig(shim) };
|
|
389
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
390
|
+
await writeForeignJson(file, merged, { readFile, writeFile, mkdir });
|
|
391
|
+
// Recorded beside the shim, never inside the client's file. A file holding
|
|
392
|
+
// only ACC's namespace looks the same whether ACC created it or the user did,
|
|
393
|
+
// and uninstall has to know which - but this client reads every top-level key
|
|
394
|
+
// as a namespace, and one it cannot read drops the whole file. A marker
|
|
395
|
+
// written the way the Gemini CLI adapter writes its own registered nothing at
|
|
396
|
+
// all, valid namespace included: fixtures/hooks-readback-foreign-key-1.2.7.json.
|
|
397
|
+
if (found === null) {
|
|
398
|
+
await mkdir(createdMarkerDir(context), { recursive: true });
|
|
399
|
+
await writeFile(createdMarker(context, file), `${file}\n`);
|
|
400
|
+
}
|
|
401
|
+
await installSkillPlugin(context);
|
|
402
|
+
const changes = [shimDir(home), file, pluginInstallPath(home), vendorManifestPath(home)];
|
|
403
|
+
|
|
404
|
+
const diagnostics = [];
|
|
405
|
+
const needsAction = [];
|
|
406
|
+
if (locationOf(context) === "workspace") {
|
|
407
|
+
diagnostics.push("a workspace registration loads only while this project is an open "
|
|
408
|
+
+ `Antigravity workspace; in print mode pass --add-dir ${context.antigravityWorkspace}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let readback = null;
|
|
412
|
+
try {
|
|
413
|
+
readback = await probeFor(context)(context);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
// The client could not be asked. Say so and claim nothing: an unverifiable
|
|
416
|
+
// write is exactly the state this adapter exists to stop reporting as done.
|
|
417
|
+
diagnostics.push(`wrote ${file} but could not read the effective hook list back `
|
|
418
|
+
+ `(${error.message}); verify with: agy -p "/hooks" --output-format json`);
|
|
419
|
+
needsAction.push("run agy -p \"/hooks\" --output-format json and confirm the acc "
|
|
420
|
+
+ "namespace lists SessionStart, PreInvocation and Stop");
|
|
421
|
+
}
|
|
422
|
+
if (readback !== null) {
|
|
423
|
+
const loaded = registeredEvents(readback);
|
|
424
|
+
const dropped = ACC_REGISTERED_EVENTS.filter(event => !loaded.includes(event));
|
|
425
|
+
if (dropped.length > 0) {
|
|
426
|
+
throw new AccError(EXIT.DATA,
|
|
427
|
+
loaded.length === 0
|
|
428
|
+
? `${file} was written and registered nothing: agy reports no enabled `
|
|
429
|
+
+ `${ACC_NAMESPACE} hooks. The file parsed; this client drops a configuration `
|
|
430
|
+
+ "it does not recognise without logging anything."
|
|
431
|
+
: `${file} is only partly registered: agy loaded ${loaded.join(", ")} and dropped `
|
|
432
|
+
+ `${dropped.join(", ")}.`,
|
|
433
|
+
{ file, loaded, dropped, source: registeredSource(readback) });
|
|
434
|
+
}
|
|
435
|
+
diagnostics.push(`acc hooks registered: ${loaded.join(", ")} loaded from `
|
|
436
|
+
+ `${registeredSource(readback) ?? file}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let skills = null;
|
|
440
|
+
try {
|
|
441
|
+
skills = await probeInstalledSkills(context);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
diagnostics.push(`installed the ${ACC_PLUGIN_NAME} plugin but could not read the skill `
|
|
444
|
+
+ `list back (${error.message}); verify with: agy -p "/skills" --output-format json`);
|
|
445
|
+
needsAction.push(`run agy -p "/skills" --output-format json and confirm ${ACC_SKILL_ID} `
|
|
446
|
+
+ "is listed");
|
|
447
|
+
}
|
|
448
|
+
if (skills !== null) {
|
|
449
|
+
const skill = registeredSkill(skills, home);
|
|
450
|
+
if (skill === null) {
|
|
451
|
+
throw new AccError(EXIT.DATA,
|
|
452
|
+
`installed the ${ACC_PLUGIN_NAME} plugin and the client does not list ${ACC_SKILL_ID} `
|
|
453
|
+
+ `from ${pluginInstallPath(home)}: the copy is on disk and not loaded`,
|
|
454
|
+
{ plugin: pluginInstallPath(home) });
|
|
455
|
+
}
|
|
456
|
+
diagnostics.push(`acc skill registered: ${ACC_SKILL_ID} loaded from ${skill.path}`);
|
|
457
|
+
}
|
|
458
|
+
return { ok: true, changes, diagnostics, ...(needsAction.length > 0 ? { needsAction } : {}) };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Remove what this adapter wrote, and nothing else.
|
|
463
|
+
*
|
|
464
|
+
* Ownership is the `acc` namespace key. Every other key in the file belongs to
|
|
465
|
+
* somebody else and is written back exactly as it was. Nothing here reads or
|
|
466
|
+
* writes `~/.gemini/settings.json` or `~/.gemini/extensions/`: those are the
|
|
467
|
+
* Gemini CLI integration's, in a tree the two clients share.
|
|
468
|
+
*/
|
|
469
|
+
export async function uninstallAntigravity(context) {
|
|
470
|
+
const { home, keep = [] } = context;
|
|
471
|
+
const changes = [];
|
|
472
|
+
const diagnostics = [];
|
|
473
|
+
// Nothing may keep running once ACC is gone. A relay holds a live session
|
|
474
|
+
// endpoint in memory, so it is stopped here rather than left to find out.
|
|
475
|
+
const control = context.antigravityRelayControl ?? {};
|
|
476
|
+
const stopped = await stopRelays({ dataHome: context.dataHome, ...control }).catch(() => 0);
|
|
477
|
+
if (stopped > 0) diagnostics.push(`stopped ${stopped} antigravity relay(s)`);
|
|
478
|
+
for (const file of [globalHooksPath(home),
|
|
479
|
+
...(typeof context.antigravityWorkspace === "string"
|
|
480
|
+
? [workspaceHooksPath(context.antigravityWorkspace)] : [])]) {
|
|
481
|
+
const existing = await readJson(file, null);
|
|
482
|
+
if (existing === null || !Object.hasOwn(existing, ACC_NAMESPACE)) continue;
|
|
483
|
+
const next = { ...existing };
|
|
484
|
+
delete next[ACC_NAMESPACE];
|
|
485
|
+
await writeForeignJson(file, next, { readFile, writeFile, mkdir });
|
|
486
|
+
const marker = createdMarker(context, file);
|
|
487
|
+
const created = await exists(marker);
|
|
488
|
+
await removeIfEmpty(file, { readFile, rm, isEmpty: blankJson(), created });
|
|
489
|
+
if (created) await rm(marker, { force: true });
|
|
490
|
+
changes.push(file);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// The plugin goes back through the client that installed it, so its manifest
|
|
494
|
+
// entry goes with it. Removing the directory by hand would leave agy listing a
|
|
495
|
+
// plugin that is not there. Only when agy itself cannot be run is the copy
|
|
496
|
+
// removed directly - an ACC skill whose command is about to disappear must
|
|
497
|
+
// not stay behind for a model to follow.
|
|
498
|
+
if (await exists(pluginInstallPath(home))) {
|
|
499
|
+
try {
|
|
500
|
+
await agyFor(context)(["plugin", "uninstall", ACC_PLUGIN_NAME],
|
|
501
|
+
{ home, env: context.env });
|
|
502
|
+
} catch (error) {
|
|
503
|
+
await rm(pluginInstallPath(home), { recursive: true, force: true });
|
|
504
|
+
diagnostics.push(`agy plugin uninstall ${ACC_PLUGIN_NAME} failed (${error.message}); `
|
|
505
|
+
+ `removed ${pluginInstallPath(home)} directly, and agy may still list it`);
|
|
506
|
+
}
|
|
507
|
+
changes.push(pluginInstallPath(home));
|
|
508
|
+
}
|
|
509
|
+
// What agy leaves after the last uninstall: `{"imports": null}`, in a file
|
|
510
|
+
// that did not exist before ACC's install. Taken away only when ACC's install
|
|
511
|
+
// is why it exists and nothing but that null is left in it.
|
|
512
|
+
const manifest = vendorManifestPath(home);
|
|
513
|
+
const manifestMarker = createdMarker(context, manifest);
|
|
514
|
+
if (await exists(manifestMarker)) {
|
|
515
|
+
const current = await readJson(manifest, null).catch(() => undefined);
|
|
516
|
+
const empty = current !== undefined && current !== null
|
|
517
|
+
&& Object.keys(current).every(key => key === "imports")
|
|
518
|
+
&& (current.imports === null || (Array.isArray(current.imports)
|
|
519
|
+
&& current.imports.length === 0));
|
|
520
|
+
if (empty) {
|
|
521
|
+
await rm(manifest, { force: true });
|
|
522
|
+
changes.push(manifest);
|
|
523
|
+
}
|
|
524
|
+
await rm(manifestMarker, { force: true });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!keep.includes(shimDir(home))) await rm(shimDir(home), { recursive: true, force: true });
|
|
528
|
+
return { ok: true, changes, diagnostics };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* What is true on this machine, read from the client rather than from disk.
|
|
533
|
+
*
|
|
534
|
+
* The state issue #176 describes is the one worth naming: AGY copies ACC's
|
|
535
|
+
* Gemini CLI extension into its own plugin directory on first authenticated
|
|
536
|
+
* run, byte for byte, and registers none of it. A machine can therefore hold a
|
|
537
|
+
* complete copy of the ACC integration while ACC is invisible to the client.
|
|
538
|
+
* "The files are there" has to stop being read as "it is installed".
|
|
539
|
+
*/
|
|
540
|
+
/**
|
|
541
|
+
* Whether this install can proceed as asked.
|
|
542
|
+
*
|
|
543
|
+
* Only an unrecognised location blocks it. Unset is not unrecognised - that is
|
|
544
|
+
* the default, `global`. A misspelling is, and it is named rather than silently
|
|
545
|
+
* replaced: an operator who asked for `workspace` and got the machine-wide file
|
|
546
|
+
* has no way of finding out except by reading the file.
|
|
547
|
+
*/
|
|
548
|
+
export function locationChoice(context) {
|
|
549
|
+
const location = locationOf(context);
|
|
550
|
+
return HOOK_LOCATIONS.includes(location) ? null
|
|
551
|
+
: { reason: `ACC_ANTIGRAVITY_HOOKS is ${JSON.stringify(location)}, which is not a place `
|
|
552
|
+
+ "this client reads hooks from: use global "
|
|
553
|
+
+ `(${globalHooksPath(String(context?.home ?? "~"))}, always loads, applies to every `
|
|
554
|
+
+ "Antigravity session on this machine) or workspace (<project>/.agents/hooks.json, "
|
|
555
|
+
+ "scoped to one project and loaded only while it is an open Antigravity workspace)" };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function detectAntigravity(context) {
|
|
559
|
+
const diagnostics = [];
|
|
560
|
+
const needsAction = [];
|
|
561
|
+
const blocked = locationChoice(context);
|
|
562
|
+
const imported = await exists(importedPluginPath(context.home));
|
|
563
|
+
// The installer runs every adapter's detect and passes a null version for a
|
|
564
|
+
// client it did not find. Asking agy then would spawn a binary the installer
|
|
565
|
+
// could not run a moment ago, or - where it does exist but did not answer -
|
|
566
|
+
// let it write its own state into a home nobody asked it about.
|
|
567
|
+
if (context.clientVersion === null) {
|
|
568
|
+
diagnostics.push("acc hooks not registered: Antigravity CLI was not found, so it was "
|
|
569
|
+
+ "not asked what it has loaded");
|
|
570
|
+
if (blocked !== null) diagnostics.push(blocked.reason);
|
|
571
|
+
return { ok: true, changes: [], diagnostics, ...(blocked === null ? {} : { blocked }) };
|
|
572
|
+
}
|
|
573
|
+
let readback = null;
|
|
574
|
+
try {
|
|
575
|
+
readback = await probeFor(context)(context);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
diagnostics.push(`acc hooks not registered: could not ask agy what it has loaded `
|
|
578
|
+
+ `(${error.message})`);
|
|
579
|
+
}
|
|
580
|
+
if (readback !== null) {
|
|
581
|
+
const loaded = registeredEvents(readback);
|
|
582
|
+
diagnostics.push(loaded.length === ACC_REGISTERED_EVENTS.length
|
|
583
|
+
? `acc hooks registered: ${loaded.join(", ")} loaded from `
|
|
584
|
+
+ `${registeredSource(readback) ?? "an unnamed source"}`
|
|
585
|
+
: loaded.length === 0 ? "acc hooks not registered"
|
|
586
|
+
: `acc hooks not registered completely: agy loaded ${loaded.join(", ")} and is `
|
|
587
|
+
+ `missing ${ACC_REGISTERED_EVENTS.filter(e => !loaded.includes(e)).join(", ")}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let skills = null;
|
|
591
|
+
try {
|
|
592
|
+
skills = await probeInstalledSkills(context);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
diagnostics.push(`acc skill unverified: could not ask agy which skills it loads `
|
|
595
|
+
+ `(${error.message})`);
|
|
596
|
+
}
|
|
597
|
+
const hooksLoaded = readback !== null && registeredEvents(readback).length > 0;
|
|
598
|
+
if (skills !== null) {
|
|
599
|
+
const skill = registeredSkill(skills, context.home);
|
|
600
|
+
if (skill !== null) {
|
|
601
|
+
diagnostics.push(`acc skill registered: ${ACC_SKILL_ID} loaded from ${skill.path}`);
|
|
602
|
+
} else {
|
|
603
|
+
diagnostics.push(`acc skill ${ACC_SKILL_ID} is not loaded`);
|
|
604
|
+
// Hooks without the skill deliver peer messages to an agent that has no
|
|
605
|
+
// instructions for answering them. That is a reinstall, not a footnote.
|
|
606
|
+
if (hooksLoaded) {
|
|
607
|
+
needsAction.push(`acc install --adapter antigravity # ${ACC_SKILL_ID} is not loaded, `
|
|
608
|
+
+ "so an agent here receives peer messages with no instructions for answering");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const copy = importedCopy(skills);
|
|
612
|
+
if (copy !== null) {
|
|
613
|
+
diagnostics.push(`${IMPORTED_COPY_SKILL_ID} is this client's own copy of ACC's Gemini `
|
|
614
|
+
+ `CLI extension, imported on first run from ${copy.path}. It is loaded, but it is not `
|
|
615
|
+
+ `ACC's: every command in it runs the Gemini CLI extension's shim, so it stops `
|
|
616
|
+
+ `working when Gemini CLI is unwired. ${ACC_SKILL_ID} is the one ACC installs here`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (imported && !hooksLoaded) {
|
|
620
|
+
diagnostics.push("an imported plugin directory for agents-can-communicate exists at "
|
|
621
|
+
+ `${importedPluginPath(context.home)}: this client copies the Gemini CLI extension on `
|
|
622
|
+
+ "first run and loads its skill, but none of its hook names, so no ACC hook runs here "
|
|
623
|
+
+ "(issue #176)");
|
|
624
|
+
}
|
|
625
|
+
if (blocked !== null) {
|
|
626
|
+
diagnostics.push(blocked.reason);
|
|
627
|
+
needsAction.push(blocked.reason);
|
|
628
|
+
}
|
|
629
|
+
return { ok: true, changes: [], diagnostics,
|
|
630
|
+
...(blocked === null ? {} : { blocked }),
|
|
631
|
+
...(needsAction.length > 0 ? { needsAction } : {}) };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export async function doctorAntigravity(context) {
|
|
635
|
+
const detected = await detectAntigravity(context);
|
|
636
|
+
const diagnostics = [...detected.diagnostics,
|
|
637
|
+
"hook payloads carry no hook_event_name; each registered command passes its own event name",
|
|
638
|
+
"only SessionStart, PreInvocation, PostInvocation and Stop load; SessionEnd, PreToolUse "
|
|
639
|
+
+ "and PostToolUse are accepted into the file and silently dropped, so this client has "
|
|
640
|
+
+ "no tool guard and no session end",
|
|
641
|
+
"Stop continuation is a bounded nudge, not a gate: this adapter continues a turn at most "
|
|
642
|
+
+ "once and fails open, and the client caps consecutive continuations itself (1.1.9)",
|
|
643
|
+
// The one failure a healthy registration can still produce, and the reason
|
|
644
|
+
// this line exists at all: the hook runs, finds no project in the payload,
|
|
645
|
+
// fails open, and this client does not show hook stderr. Nothing else on
|
|
646
|
+
// the machine would ever tell the operator.
|
|
647
|
+
"hooks only attach a session when the Antigravity session has an open workspace: the "
|
|
648
|
+
+ "TUI started in a project directory has one, but a print-mode `agy -p` there sends an "
|
|
649
|
+
+ "empty workspacePaths and the hook has no project to join. Open the project as an "
|
|
650
|
+
+ "Antigravity workspace, or pass it with --add-dir. The first TUI session in a folder "
|
|
651
|
+
+ "trusted at that same launch sends none either, even with --add-dir: start the TUI "
|
|
652
|
+
+ "again once the folder is trusted"];
|
|
653
|
+
if (locationOf(context) === "workspace") {
|
|
654
|
+
diagnostics.push("registered per workspace; a session opened anywhere else loads nothing");
|
|
655
|
+
}
|
|
656
|
+
diagnostics.push(`live delivery starts when the agent runs sh "${relayShimPath(context.home)}" `
|
|
657
|
+
+ "start once in a conversation; ACC asks it to, in its context line, when the policy is on");
|
|
658
|
+
diagnostics.push(`antigravity relays running on this machine: ${await runningRelays({
|
|
659
|
+
dataHome: context.dataHome })}`);
|
|
660
|
+
return { ok: true, changes: [], diagnostics };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* The paths an install would write, without writing them. Same helpers as the
|
|
665
|
+
* install, so a dry run cannot drift from what actually happens.
|
|
666
|
+
*/
|
|
667
|
+
export function planAntigravityInstall(context) {
|
|
668
|
+
const shim = { path: shimDir(context.home), kind: "tree" };
|
|
669
|
+
// With no location chosen there is no install to describe, and an uninstall
|
|
670
|
+
// still has to be describable: the planner asks for the artifacts of a client
|
|
671
|
+
// that is present whichever way the command is going. So both candidates are
|
|
672
|
+
// named - which is what uninstall already scans - rather than throwing and
|
|
673
|
+
// making a recorded install unremovable. An install never reaches here
|
|
674
|
+
// unchosen; `locationChoice` blocks it one step earlier.
|
|
675
|
+
// The plugin and the manifest agy records it in are the client's, reached
|
|
676
|
+
// only through `agy plugin`. Declared as delegated - `merge` is the kind the
|
|
677
|
+
// installer leaves to the adapter - so the installer never deletes the copy
|
|
678
|
+
// ahead of `agy plugin uninstall` and strands its manifest entry. That exact
|
|
679
|
+
// ordering, with the shim directory, once left `{}` behind in the user's home.
|
|
680
|
+
const plugin = [{ path: pluginInstallPath(context.home), kind: "merge" },
|
|
681
|
+
{ path: vendorManifestPath(context.home), kind: "merge" }];
|
|
682
|
+
if (locationChoice(context) !== null) {
|
|
683
|
+
return [shim, ...plugin,
|
|
684
|
+
{ path: globalHooksPath(context.home), kind: "merge" },
|
|
685
|
+
...(typeof context.antigravityWorkspace === "string" && context.antigravityWorkspace !== ""
|
|
686
|
+
? [{ path: workspaceHooksPath(context.antigravityWorkspace), kind: "merge" }] : [])];
|
|
687
|
+
}
|
|
688
|
+
return [shim, ...plugin, { path: hooksPathFor(context), kind: "merge" }];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Refuse a removal that would take somebody else's file with it.
|
|
693
|
+
*
|
|
694
|
+
* Read-only, and it runs before the installer deletes any recorded artifact.
|
|
695
|
+
*/
|
|
696
|
+
export async function preflightAntigravityUninstall(context) {
|
|
697
|
+
for (const file of [globalHooksPath(context.home),
|
|
698
|
+
...(typeof context.antigravityWorkspace === "string"
|
|
699
|
+
? [workspaceHooksPath(context.antigravityWorkspace)] : [])]) {
|
|
700
|
+
const existing = await readJson(file, null);
|
|
701
|
+
if (existing === null) continue;
|
|
702
|
+
if (typeof existing !== "object" || Array.isArray(existing)) {
|
|
703
|
+
throw new AccError(EXIT.DATA,
|
|
704
|
+
`${file} is not a hook configuration object; refusing to edit it`, { file });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return { ok: true, changes: [], diagnostics: [] };
|
|
708
|
+
}
|