@agentex/agent 0.0.38 → 0.0.39
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/CHANGELOG.md +28 -0
- package/README.md +45 -13
- package/dist/providers/antigravity/codec.d.ts +4 -0
- package/dist/providers/antigravity/codec.d.ts.map +1 -0
- package/dist/providers/antigravity/codec.js +27 -0
- package/dist/providers/antigravity/codec.js.map +1 -0
- package/dist/providers/antigravity/connection.d.ts +100 -0
- package/dist/providers/antigravity/connection.d.ts.map +1 -0
- package/dist/providers/antigravity/connection.js +267 -0
- package/dist/providers/antigravity/connection.js.map +1 -0
- package/dist/providers/antigravity/discovery.d.ts +11 -0
- package/dist/providers/antigravity/discovery.d.ts.map +1 -0
- package/dist/providers/antigravity/discovery.js +92 -0
- package/dist/providers/antigravity/discovery.js.map +1 -0
- package/dist/providers/antigravity/execute.d.ts +9 -0
- package/dist/providers/antigravity/execute.d.ts.map +1 -0
- package/dist/providers/antigravity/execute.js +206 -0
- package/dist/providers/antigravity/execute.js.map +1 -0
- package/dist/providers/antigravity/index.d.ts +12 -0
- package/dist/providers/antigravity/index.d.ts.map +1 -0
- package/dist/providers/antigravity/index.js +46 -0
- package/dist/providers/antigravity/index.js.map +1 -0
- package/dist/providers/antigravity/parse.d.ts +114 -0
- package/dist/providers/antigravity/parse.d.ts.map +1 -0
- package/dist/providers/antigravity/parse.js +369 -0
- package/dist/providers/antigravity/parse.js.map +1 -0
- package/dist/providers/antigravity/probe.d.ts +3 -0
- package/dist/providers/antigravity/probe.d.ts.map +1 -0
- package/dist/providers/antigravity/probe.js +84 -0
- package/dist/providers/antigravity/probe.js.map +1 -0
- package/dist/providers/antigravity/runtime.d.ts +30 -0
- package/dist/providers/antigravity/runtime.d.ts.map +1 -0
- package/dist/providers/antigravity/runtime.js +69 -0
- package/dist/providers/antigravity/runtime.js.map +1 -0
- package/dist/providers/antigravity/session.d.ts +7 -0
- package/dist/providers/antigravity/session.d.ts.map +1 -0
- package/dist/providers/antigravity/session.js +317 -0
- package/dist/providers/antigravity/session.js.map +1 -0
- package/dist/providers/gemini/index.d.ts.map +1 -1
- package/dist/providers/gemini/index.js +6 -0
- package/dist/providers/gemini/index.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -0
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/auth.d.ts +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +79 -1
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/binary.d.ts.map +1 -1
- package/dist/utils/binary.js +10 -0
- package/dist/utils/binary.js.map +1 -1
- package/dist/utils/instructions.d.ts.map +1 -1
- package/dist/utils/instructions.js +12 -9
- package/dist/utils/instructions.js.map +1 -1
- package/dist/utils/runtime-homes.d.ts.map +1 -1
- package/dist/utils/runtime-homes.js +4 -0
- package/dist/utils/runtime-homes.js.map +1 -1
- package/dist/utils/skills.d.ts +3 -3
- package/dist/utils/skills.d.ts.map +1 -1
- package/dist/utils/skills.js +16 -2
- package/dist/utils/skills.js.map +1 -1
- package/package.json +3 -2
- package/src/providers/antigravity/codec.ts +29 -0
- package/src/providers/antigravity/connection.ts +304 -0
- package/src/providers/antigravity/discovery.ts +90 -0
- package/src/providers/antigravity/execute.ts +214 -0
- package/src/providers/antigravity/index.ts +48 -0
- package/src/providers/antigravity/parse.ts +439 -0
- package/src/providers/antigravity/probe.ts +89 -0
- package/src/providers/antigravity/runtime.ts +64 -0
- package/src/providers/antigravity/session.ts +359 -0
- package/src/providers/gemini/index.ts +6 -0
- package/src/registry.ts +2 -0
- package/src/types.ts +6 -5
- package/src/utils/auth.ts +82 -1
- package/src/utils/binary.ts +10 -0
- package/src/utils/instructions.ts +10 -9
- package/src/utils/runtime-homes.ts +4 -0
- package/src/utils/skills.ts +17 -4
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentSession,
|
|
3
|
+
CancelResult,
|
|
4
|
+
ClearGoalResult,
|
|
5
|
+
GoalOptions,
|
|
6
|
+
GoalState,
|
|
7
|
+
SendHandle,
|
|
8
|
+
SendOptions,
|
|
9
|
+
SessionContext,
|
|
10
|
+
SessionState,
|
|
11
|
+
SetGoalResult,
|
|
12
|
+
StopTaskResult,
|
|
13
|
+
StreamEvent,
|
|
14
|
+
TurnResult,
|
|
15
|
+
TurnTrigger,
|
|
16
|
+
} from "../../types.js";
|
|
17
|
+
import { GoalController, EMULATED_GOAL_CAPABILITY } from "../../goals/index.js";
|
|
18
|
+
import type { ResolvedBinary } from "../../utils/binary.js";
|
|
19
|
+
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
20
|
+
import { resolveInstructions } from "../../utils/instructions.js";
|
|
21
|
+
import { injectHomeSkills } from "../../utils/skills.js";
|
|
22
|
+
import { uuidv7 } from "../../utils/uuid.js";
|
|
23
|
+
import {
|
|
24
|
+
AgyConnection,
|
|
25
|
+
firstAgyErrorLine,
|
|
26
|
+
interruptAgyTurn,
|
|
27
|
+
type AgyTurnOutcome,
|
|
28
|
+
} from "./connection.js";
|
|
29
|
+
import {
|
|
30
|
+
ANTIGRAVITY_PROVIDER_TYPE,
|
|
31
|
+
AgyStreamParser,
|
|
32
|
+
agyUsageRecord,
|
|
33
|
+
classifyAgyFailure,
|
|
34
|
+
mapAgyTurnStatus,
|
|
35
|
+
} from "./parse.js";
|
|
36
|
+
import { AGY_SIGN_IN_MESSAGE, buildAgyArgs, findAgyBinary, readAgyResumeId } from "./runtime.js";
|
|
37
|
+
|
|
38
|
+
interface ActiveTurn {
|
|
39
|
+
turnId: string;
|
|
40
|
+
abort: AbortController;
|
|
41
|
+
done: Promise<TurnResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open a multi-turn Antigravity session. Throws when `agy` is not installed.
|
|
46
|
+
* The process starts on the first `send()`.
|
|
47
|
+
*/
|
|
48
|
+
export async function createAntigravitySession(ctx: SessionContext): Promise<AgentSession> {
|
|
49
|
+
const binary = await findAgyBinary(ctx);
|
|
50
|
+
const config = ctx.config ?? {};
|
|
51
|
+
const instructions = await resolveInstructions(config.instructionsFile);
|
|
52
|
+
if (config.skillDirs && config.skillDirs.length > 0) {
|
|
53
|
+
try {
|
|
54
|
+
await injectHomeSkills(config.skillDirs, "antigravity");
|
|
55
|
+
} catch {
|
|
56
|
+
// Non-fatal
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return new AntigravitySession(ctx, binary, instructions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One `agy --input-format stream-json --output-format stream-json` process
|
|
64
|
+
* holds the conversation across turns. The CLI has no control protocol, so
|
|
65
|
+
* an interrupt is a SIGINT (Ctrl+C); when that ends the process, the next
|
|
66
|
+
* `send()` starts a new one on the same conversation with `--conversation`.
|
|
67
|
+
*/
|
|
68
|
+
class AntigravitySession implements AgentSession {
|
|
69
|
+
private _state: SessionState = "idle";
|
|
70
|
+
private conversationId: string | null;
|
|
71
|
+
private connection: AgyConnection | null = null;
|
|
72
|
+
private parser: AgyStreamParser | null = null;
|
|
73
|
+
private active: ActiveTurn | null = null;
|
|
74
|
+
/** Events received from agy processes, to tell whether one saw a message. */
|
|
75
|
+
private connectionEvents = 0;
|
|
76
|
+
private draining = false;
|
|
77
|
+
/** Instructions go into the first message of a conversation this session starts. */
|
|
78
|
+
private pendingInstructions: string | null;
|
|
79
|
+
private readonly cwd: string;
|
|
80
|
+
private readonly goals: GoalController;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly ctx: SessionContext,
|
|
84
|
+
private readonly binary: ResolvedBinary,
|
|
85
|
+
instructions: string | null,
|
|
86
|
+
) {
|
|
87
|
+
this.cwd = ctx.cwd ?? process.cwd();
|
|
88
|
+
this.conversationId = readAgyResumeId(ctx.sessionParams, this.cwd);
|
|
89
|
+
this.pendingInstructions = this.conversationId ? null : instructions;
|
|
90
|
+
this.goals = new GoalController({
|
|
91
|
+
providerType: ANTIGRAVITY_PROVIDER_TYPE,
|
|
92
|
+
capability: EMULATED_GOAL_CAPABILITY,
|
|
93
|
+
getSessionId: () => this.sessionId,
|
|
94
|
+
send: (message) => this.send(message),
|
|
95
|
+
dispatch: (event) => void this.dispatch(event),
|
|
96
|
+
});
|
|
97
|
+
if (ctx.signal) {
|
|
98
|
+
if (ctx.signal.aborted) void this.close();
|
|
99
|
+
else ctx.signal.addEventListener("abort", () => void this.close(), { once: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get sessionId(): string | null {
|
|
104
|
+
return this.parser?.conversationId ?? this.conversationId;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get state(): SessionState {
|
|
108
|
+
return this._state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async send(message: string, options?: SendOptions): Promise<SendHandle> {
|
|
112
|
+
if (this._state === "closed") throw new Error("Session is closed");
|
|
113
|
+
if (this.draining) throw new Error("Session is draining — no new sends accepted");
|
|
114
|
+
if (this.active) {
|
|
115
|
+
throw new Error("Antigravity session is busy — a turn is already in progress (concurrentSend not supported)");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const uuid = uuidv7();
|
|
119
|
+
const turnId = uuidv7();
|
|
120
|
+
const abort = new AbortController();
|
|
121
|
+
const text = this.pendingInstructions ? `${this.pendingInstructions}\n\n${message}` : message;
|
|
122
|
+
this.pendingInstructions = null;
|
|
123
|
+
|
|
124
|
+
this._state = "thinking";
|
|
125
|
+
const done = this.runTurn(text, turnId, abort, options);
|
|
126
|
+
this.active = { turnId, abort, done };
|
|
127
|
+
void done.then((result) => this.goals.onTurnSettled(result)).catch(() => undefined);
|
|
128
|
+
return { uuid, result: done };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private ensureConnection(): AgyConnection {
|
|
132
|
+
if (this.connection?.alive) return this.connection;
|
|
133
|
+
const config = this.ctx.config ?? {};
|
|
134
|
+
// A replacement process picks the conversation back up where it stopped.
|
|
135
|
+
this.conversationId = this.sessionId;
|
|
136
|
+
const parser = new AgyStreamParser({
|
|
137
|
+
includePartialMessages: config.includePartialMessages,
|
|
138
|
+
conversationId: this.conversationId,
|
|
139
|
+
});
|
|
140
|
+
const env = buildEnv(this.ctx.env);
|
|
141
|
+
ensurePathInEnv(env);
|
|
142
|
+
this.ctx.onLifecycle?.({ phase: "spawning" });
|
|
143
|
+
const connection = new AgyConnection({
|
|
144
|
+
command: this.binary.bin,
|
|
145
|
+
args: [
|
|
146
|
+
...this.binary.prefixArgs,
|
|
147
|
+
...buildAgyArgs(config, { resumeId: this.conversationId, model: config.model ?? null }),
|
|
148
|
+
],
|
|
149
|
+
cwd: this.cwd,
|
|
150
|
+
env,
|
|
151
|
+
parser,
|
|
152
|
+
// Lines a replaced process prints on its way out belong to no turn.
|
|
153
|
+
onEvent: async (event): Promise<void> => {
|
|
154
|
+
if (this.connection === connection) await this.onEvent(event);
|
|
155
|
+
},
|
|
156
|
+
...(this.ctx.onOutput ? { onOutput: this.ctx.onOutput } : {}),
|
|
157
|
+
});
|
|
158
|
+
if (connection.pid != null) this.ctx.onLifecycle?.({ phase: "running", pid: connection.pid });
|
|
159
|
+
this.connection = connection;
|
|
160
|
+
this.parser = parser;
|
|
161
|
+
return connection;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async runTurn(
|
|
165
|
+
text: string,
|
|
166
|
+
turnId: string,
|
|
167
|
+
abort: AbortController,
|
|
168
|
+
options?: SendOptions,
|
|
169
|
+
): Promise<TurnResult> {
|
|
170
|
+
await this.emitTurnEdge({ type: "turn_start", turnId, trigger: "send" });
|
|
171
|
+
|
|
172
|
+
const timeoutSec = options?.timeoutSec ?? this.ctx.config?.timeoutSec;
|
|
173
|
+
const graceSec = this.ctx.config?.graceSec;
|
|
174
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
175
|
+
const guards: Promise<"timeout" | "aborted">[] = [
|
|
176
|
+
new Promise((resolve) => abort.signal.addEventListener("abort", () => resolve("aborted"), { once: true })),
|
|
177
|
+
];
|
|
178
|
+
if (timeoutSec && timeoutSec > 0) {
|
|
179
|
+
guards.push(new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), timeoutSec * 1000); }));
|
|
180
|
+
}
|
|
181
|
+
const onSendAbort = () => abort.abort();
|
|
182
|
+
if (options?.signal) {
|
|
183
|
+
if (options.signal.aborted) abort.abort();
|
|
184
|
+
else options.signal.addEventListener("abort", onSendAbort, { once: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let connection = this.ensureConnection();
|
|
188
|
+
try {
|
|
189
|
+
let result: TurnResult | null = null;
|
|
190
|
+
let reason: "result" | "cancelled" | "session_closed" = "result";
|
|
191
|
+
for (let attempt = 0; result === null; attempt++) {
|
|
192
|
+
const eventsBefore = this.connectionEvents;
|
|
193
|
+
const pending = connection.nextTurn();
|
|
194
|
+
connection.write(text);
|
|
195
|
+
const first = await Promise.race([pending, ...guards]);
|
|
196
|
+
if (first === "timeout" || first === "aborted") {
|
|
197
|
+
const outcome = await interruptAgyTurn(connection, pending, graceSec);
|
|
198
|
+
// agy may report INTERRUPTED and then exit. Learn which before the
|
|
199
|
+
// next send, so it never lands on a process that is going away.
|
|
200
|
+
if (outcome?.result) await connection.waitForExit(500);
|
|
201
|
+
// Without an INTERRUPTED result the process was stopped; a late result
|
|
202
|
+
// from it must never settle the next turn.
|
|
203
|
+
if ((!outcome?.result || !connection.alive) && this.connection === connection) this.connection = null;
|
|
204
|
+
result = {
|
|
205
|
+
summary: outcome?.result?.response.trim() || null,
|
|
206
|
+
...this.usageOf(outcome),
|
|
207
|
+
costUsd: null,
|
|
208
|
+
status: first,
|
|
209
|
+
errorCode: first,
|
|
210
|
+
errorMessage: first === "timeout" ? "Turn exceeded its timeout" : "Turn aborted",
|
|
211
|
+
};
|
|
212
|
+
reason = "cancelled";
|
|
213
|
+
} else if (
|
|
214
|
+
attempt === 0 && !first.result && first.exited && !first.authRequired
|
|
215
|
+
&& this.connectionEvents === eventsBefore && this._state !== "closed"
|
|
216
|
+
) {
|
|
217
|
+
// The process exited without ever acknowledging the message (it was
|
|
218
|
+
// already shutting down). Run it on a fresh process for the same
|
|
219
|
+
// conversation instead of failing a turn agy never started.
|
|
220
|
+
if (this.connection === connection) this.connection = null;
|
|
221
|
+
connection = this.ensureConnection();
|
|
222
|
+
} else {
|
|
223
|
+
result = this.toTurnResult(first);
|
|
224
|
+
reason = first.result ? "result" : "session_closed";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
await this.emitTurnEdge({ type: "turn_end", turnId, trigger: "send", reason });
|
|
228
|
+
return result;
|
|
229
|
+
} finally {
|
|
230
|
+
if (timer) clearTimeout(timer);
|
|
231
|
+
options?.signal?.removeEventListener("abort", onSendAbort);
|
|
232
|
+
this.active = null;
|
|
233
|
+
if (!connection.alive && this.connection === connection) this.connection = null;
|
|
234
|
+
if (this._state !== "closed") this._state = "idle";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private usageOf(outcome: AgyTurnOutcome | null): { usage?: TurnResult["usage"] } {
|
|
239
|
+
const usage = agyUsageRecord(outcome?.result?.turnUsage ?? null, this.parser?.model ?? this.ctx.config?.model ?? null);
|
|
240
|
+
return usage ? { usage } : {};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private toTurnResult(outcome: AgyTurnOutcome): TurnResult {
|
|
244
|
+
const { result } = outcome;
|
|
245
|
+
if (result) {
|
|
246
|
+
const status = mapAgyTurnStatus(result.status);
|
|
247
|
+
if (status === "completed") {
|
|
248
|
+
return { summary: result.response.trim() || null, ...this.usageOf(outcome), costUsd: null, status, errorCode: null, errorMessage: null };
|
|
249
|
+
}
|
|
250
|
+
const errorCode = status === "aborted"
|
|
251
|
+
? "aborted"
|
|
252
|
+
: outcome.authRequired ? "auth_required" : classifyAgyFailure(result, outcome.agyError);
|
|
253
|
+
return {
|
|
254
|
+
summary: result.response.trim() || null,
|
|
255
|
+
...this.usageOf(outcome),
|
|
256
|
+
costUsd: null,
|
|
257
|
+
status,
|
|
258
|
+
errorCode,
|
|
259
|
+
errorMessage: errorCode === "auth_required"
|
|
260
|
+
? AGY_SIGN_IN_MESSAGE
|
|
261
|
+
: result.error ?? outcome.agyError?.message ?? `Antigravity turn ended with ${result.status}`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (outcome.authRequired) {
|
|
265
|
+
return { summary: null, costUsd: null, status: "failed", errorCode: "auth_required", errorMessage: AGY_SIGN_IN_MESSAGE };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
summary: null,
|
|
269
|
+
costUsd: null,
|
|
270
|
+
status: "failed",
|
|
271
|
+
errorCode: "process_exited",
|
|
272
|
+
errorMessage: outcome.agyError?.message
|
|
273
|
+
?? firstAgyErrorLine(outcome.stderr)
|
|
274
|
+
?? `Antigravity CLI exited (code ${outcome.exitCode ?? "null"}${outcome.signal ? `, ${outcome.signal}` : ""}) before finishing the turn`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private async onEvent(event: StreamEvent): Promise<void> {
|
|
279
|
+
this.connectionEvents++;
|
|
280
|
+
if (this._state !== "closed") {
|
|
281
|
+
if (event.type === "tool_call") this._state = "tool_executing";
|
|
282
|
+
else if (event.type === "assistant" || event.type === "thinking" || event.type === "tool_result") this._state = "thinking";
|
|
283
|
+
}
|
|
284
|
+
await this.dispatch(event);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async emitTurnEdge(
|
|
288
|
+
edge:
|
|
289
|
+
| { type: "turn_start"; turnId: string; trigger: TurnTrigger }
|
|
290
|
+
| { type: "turn_end"; turnId: string; trigger: TurnTrigger; reason: "result" | "cancelled" | "session_closed" },
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
await this.dispatch({
|
|
293
|
+
...edge,
|
|
294
|
+
timestamp: new Date().toISOString(),
|
|
295
|
+
providerType: ANTIGRAVITY_PROVIDER_TYPE,
|
|
296
|
+
sessionId: this.sessionId,
|
|
297
|
+
messageId: null,
|
|
298
|
+
eventId: null,
|
|
299
|
+
parentToolCallId: null,
|
|
300
|
+
raw: {},
|
|
301
|
+
} as StreamEvent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async dispatch(event: StreamEvent): Promise<void> {
|
|
305
|
+
if (!this.ctx.onEvent) return;
|
|
306
|
+
try {
|
|
307
|
+
await this.ctx.onEvent(event);
|
|
308
|
+
} catch {
|
|
309
|
+
/* a throwing handler must not break the stream */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async cancel(_uuid: string): Promise<CancelResult> {
|
|
314
|
+
// One message per turn and no queue, so there is never a queued message to drop.
|
|
315
|
+
return { cancelled: false };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async stopTask(_taskId: string): Promise<StopTaskResult> {
|
|
319
|
+
return { stopped: false };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
setGoal(objective: string, options?: GoalOptions): Promise<SetGoalResult> {
|
|
323
|
+
return this.goals.setGoal(objective, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
clearGoal(options?: { reason?: "cleared" | "blocked" }): Promise<ClearGoalResult> {
|
|
327
|
+
return this.goals.clearGoal(options);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getGoal(): GoalState | null {
|
|
331
|
+
return this.goals.getGoal();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async interrupt(): Promise<void> {
|
|
335
|
+
const active = this.active;
|
|
336
|
+
if (!active) return;
|
|
337
|
+
active.abort.abort();
|
|
338
|
+
await active.done.catch(() => undefined);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async drain(): Promise<void> {
|
|
342
|
+
this.draining = true;
|
|
343
|
+
if (this.active) await this.active.done.catch(() => undefined);
|
|
344
|
+
await this.close();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async close(): Promise<void> {
|
|
348
|
+
if (this._state === "closed" && !this.connection) return;
|
|
349
|
+
this._state = "closed";
|
|
350
|
+
const active = this.active;
|
|
351
|
+
if (active) {
|
|
352
|
+
active.abort.abort();
|
|
353
|
+
await active.done.catch(() => undefined);
|
|
354
|
+
}
|
|
355
|
+
const connection = this.connection;
|
|
356
|
+
this.connection = null;
|
|
357
|
+
if (connection) await connection.terminate(this.ctx.config?.graceSec);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -7,6 +7,12 @@ import { resolveAuthForProvider } from "../../utils/auth.js";
|
|
|
7
7
|
* `@google/gemini-cli` on PATH; Gemini handles its own Google auth (GEMINI_API_KEY,
|
|
8
8
|
* GOOGLE_API_KEY, or an OAuth login).
|
|
9
9
|
*
|
|
10
|
+
* Since 2026-06-18 Gemini CLI no longer serves free, Google AI Pro, or Google
|
|
11
|
+
* AI Ultra sign-ins ("This client is no longer supported for Gemini Code
|
|
12
|
+
* Assist for individuals"); those accounts moved to the Antigravity CLI — use
|
|
13
|
+
* the `antigravity` provider. Gemini CLI still works with a paid API key and
|
|
14
|
+
* Gemini Code Assist Standard/Enterprise.
|
|
15
|
+
*
|
|
10
16
|
* Replaces the previous one-shot `--output-format stream-json` adapter: the ACP
|
|
11
17
|
* base gives gemini real sessions, streaming, tool-call correlation, permission
|
|
12
18
|
* bridging (via `onUserInputRequest`), and mode discovery — none of which the
|
package/src/registry.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { cursorProvider } from "./providers/cursor/index.js";
|
|
|
8
8
|
import { opencodeProvider } from "./providers/opencode/index.js";
|
|
9
9
|
import { piProvider } from "./providers/pi/index.js";
|
|
10
10
|
import { copilotProvider } from "./providers/copilot/index.js";
|
|
11
|
+
import { antigravityProvider } from "./providers/antigravity/index.js";
|
|
11
12
|
|
|
12
13
|
const providers = new Map<string, ProviderModule>();
|
|
13
14
|
|
|
@@ -21,6 +22,7 @@ providers.set("cursor", cursorProvider);
|
|
|
21
22
|
providers.set("opencode", opencodeProvider);
|
|
22
23
|
providers.set("pi", piProvider);
|
|
23
24
|
providers.set("copilot", copilotProvider);
|
|
25
|
+
providers.set("antigravity", antigravityProvider);
|
|
24
26
|
|
|
25
27
|
export function getProvider(type: string): ProviderModule {
|
|
26
28
|
const provider = providers.get(type);
|
package/src/types.ts
CHANGED
|
@@ -667,8 +667,8 @@ export interface ProviderConfig {
|
|
|
667
667
|
model?: string;
|
|
668
668
|
/** Provider-native model variant, independent from reasoning effort. */
|
|
669
669
|
modelVariant?: string;
|
|
670
|
-
/** Provider reasoning effort. Claude
|
|
671
|
-
* it as the app-server `turn/start.effort` override. */
|
|
670
|
+
/** Provider reasoning effort. Claude and Antigravity map this to `--effort`;
|
|
671
|
+
* Codex sends it as the app-server `turn/start.effort` override. */
|
|
672
672
|
effort?: string;
|
|
673
673
|
/** Behavior when a host does not provide or loses its input callback. */
|
|
674
674
|
unattendedPermissionPolicy?: "allow" | "deny";
|
|
@@ -729,15 +729,16 @@ export interface ProviderConfig {
|
|
|
729
729
|
sandbox?: boolean;
|
|
730
730
|
thinking?: string;
|
|
731
731
|
/**
|
|
732
|
-
* Provider-specific mode pass-through.
|
|
733
|
-
* `--mode <mode>` flag. Don't use this for plan mode — set `planMode: true`
|
|
732
|
+
* Provider-specific mode pass-through. Used by `cursor` and `antigravity`
|
|
733
|
+
* for their `--mode <mode>` flag. Don't use this for plan mode — set `planMode: true`
|
|
734
734
|
* instead, which is the cross-provider abstraction.
|
|
735
735
|
*/
|
|
736
736
|
mode?: string;
|
|
737
737
|
/**
|
|
738
738
|
* Select a provider operating mode by id (one of `listModes()`). Honored by
|
|
739
739
|
* providers with `capabilities.modes === true` (codex collaboration modes,
|
|
740
|
-
* ACP session modes, copilot allow-all/agent/plan
|
|
740
|
+
* ACP session modes, copilot allow-all/agent/plan, antigravity
|
|
741
|
+
* default/accept-edits/plan). Ignored otherwise.
|
|
741
742
|
* Distinct from `mode` (cursor's raw `--mode` passthrough) and `planMode`
|
|
742
743
|
* (the cross-provider read-only abstraction).
|
|
743
744
|
*/
|
package/src/utils/auth.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import * as os from "node:os";
|
|
4
5
|
import type {
|
|
@@ -94,6 +95,12 @@ export function detectAuth(providerType: string, env: Record<string, string>): R
|
|
|
94
95
|
}
|
|
95
96
|
return { method: "subscription", billingType: "subscription" };
|
|
96
97
|
}
|
|
98
|
+
case "antigravity": {
|
|
99
|
+
if (hasEnv(env, "GEMINI_API_KEY") && readAgyModelProvider() === "gemini") {
|
|
100
|
+
return { method: "api_key", billingType: "api" };
|
|
101
|
+
}
|
|
102
|
+
return { method: "subscription", billingType: "subscription" };
|
|
103
|
+
}
|
|
97
104
|
case "opencode":
|
|
98
105
|
case "pi":
|
|
99
106
|
return { method: "api_key", billingType: "api" };
|
|
@@ -434,6 +441,74 @@ async function resolveGeminiAuth(ctx?: AuthResolveContext): Promise<AuthReport>
|
|
|
434
441
|
};
|
|
435
442
|
}
|
|
436
443
|
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// Antigravity — no status subcommand, but `agy models` exits 1 with "Please
|
|
446
|
+
// sign in" when no cached sign-in exists, so its exit status is the CLI's own
|
|
447
|
+
// answer. A GEMINI_API_KEY counts only when settings.json selects the Gemini
|
|
448
|
+
// API (`"modelProvider": "gemini"`); the CLI ignores the variable otherwise.
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
/** Home directory as agy (a Go binary) resolves it: os.UserHomeDir semantics. */
|
|
452
|
+
function agyHomeDir(): string {
|
|
453
|
+
const fromEnv = process.platform === "win32" ? process.env["USERPROFILE"] : process.env["HOME"];
|
|
454
|
+
return fromEnv && fromEnv.trim() ? fromEnv : os.homedir();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function agySettingsPath(): string {
|
|
458
|
+
return path.join(agyHomeDir(), ".gemini", "antigravity-cli", "settings.json");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function readAgyModelProvider(): string | null {
|
|
462
|
+
try {
|
|
463
|
+
const settings = JSON.parse(readFileSync(agySettingsPath(), "utf-8")) as Record<string, unknown>;
|
|
464
|
+
return typeof settings["modelProvider"] === "string" ? settings["modelProvider"] : null;
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function resolveAntigravityAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
471
|
+
const env = buildEnv(ctx?.env);
|
|
472
|
+
const binary = await checkBinary("agy", ctx?.command, env);
|
|
473
|
+
const apiKeyProvider = readAgyModelProvider() === "gemini";
|
|
474
|
+
let signedIn = false;
|
|
475
|
+
let usedCli = false;
|
|
476
|
+
if (binary.installed && binary.resolvedPath) {
|
|
477
|
+
try {
|
|
478
|
+
const result = await runChildProcess({
|
|
479
|
+
runId: "antigravity-auth-status",
|
|
480
|
+
command: binary.resolvedPath,
|
|
481
|
+
args: [...binary.prefixArgs, "models"],
|
|
482
|
+
cwd: process.cwd(),
|
|
483
|
+
env,
|
|
484
|
+
timeoutSec: 20,
|
|
485
|
+
});
|
|
486
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
487
|
+
signedIn = result.exitCode === 0 && !/sign in|authentication required/i.test(output);
|
|
488
|
+
usedCli = !result.timedOut;
|
|
489
|
+
} catch {
|
|
490
|
+
// Presence stays false; the binary could not be run.
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
providerType: "antigravity",
|
|
495
|
+
binary: stripPrefixArgs(binary),
|
|
496
|
+
options: [
|
|
497
|
+
{
|
|
498
|
+
method: "subscription",
|
|
499
|
+
source: { kind: "cli", command: "agy models" },
|
|
500
|
+
present: signedIn && !apiKeyProvider,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
method: "api_key",
|
|
504
|
+
source: { kind: "env", var: "GEMINI_API_KEY" },
|
|
505
|
+
present: apiKeyProvider && hasEnv(env, "GEMINI_API_KEY"),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
source: usedCli ? "cli" : "filesystem",
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
437
512
|
// ---------------------------------------------------------------------------
|
|
438
513
|
// Cursor uses its selected binary's `status` command. Other providers in this
|
|
439
514
|
// section use environment or native-store presence reports.
|
|
@@ -538,6 +613,8 @@ async function resolveInternal(providerType: string, ctx?: AuthResolveContext):
|
|
|
538
613
|
return resolveGeminiAuth(ctx);
|
|
539
614
|
case "cursor":
|
|
540
615
|
return resolveCursorAuth(ctx);
|
|
616
|
+
case "antigravity":
|
|
617
|
+
return resolveAntigravityAuth(ctx);
|
|
541
618
|
case "opencode":
|
|
542
619
|
return resolveOpencodeAuth(ctx);
|
|
543
620
|
case "pi":
|
|
@@ -626,7 +703,7 @@ export async function hasBedrock(
|
|
|
626
703
|
* Shell command a host should surface when an `auth_required` event fires
|
|
627
704
|
* for `providerType`, or when `isLoggedIn` returns false. Values are the
|
|
628
705
|
* provider's canonical CLI subcommand for interactive login — they spawn
|
|
629
|
-
* a browser flow (Claude, Codex, Gemini, Cursor) or print API-key setup
|
|
706
|
+
* a browser flow (Claude, Codex, Gemini, Cursor, Antigravity) or print API-key setup
|
|
630
707
|
* instructions (OpenCode, Pi).
|
|
631
708
|
*
|
|
632
709
|
* Unknown provider types fall back to `${providerType} login`, which is
|
|
@@ -647,6 +724,10 @@ export function loginCommandFor(providerType: string): string {
|
|
|
647
724
|
return "gemini";
|
|
648
725
|
case "cursor":
|
|
649
726
|
return "cursor-agent login";
|
|
727
|
+
case "antigravity":
|
|
728
|
+
// No login subcommand: running `agy` signs in through the browser and
|
|
729
|
+
// caches the session in the OS keyring for headless runs.
|
|
730
|
+
return "agy";
|
|
650
731
|
case "opencode":
|
|
651
732
|
return "opencode auth login";
|
|
652
733
|
case "pi":
|
package/src/utils/binary.ts
CHANGED
|
@@ -44,6 +44,13 @@ const COMMON_PATHS_UNIX: Record<string, string[]> = {
|
|
|
44
44
|
"/usr/local/bin/pi",
|
|
45
45
|
path.join(os.homedir(), ".npm-global", "bin", "pi"),
|
|
46
46
|
],
|
|
47
|
+
// Antigravity CLI. Its installer writes ~/.local/bin/agy, which GUI apps
|
|
48
|
+
// launched outside a login shell usually don't have on PATH.
|
|
49
|
+
agy: [
|
|
50
|
+
path.join(os.homedir(), ".local", "bin", "agy"),
|
|
51
|
+
"/usr/local/bin/agy",
|
|
52
|
+
"/opt/homebrew/bin/agy",
|
|
53
|
+
],
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
function getCommonPathsWindows(name: string): string[] {
|
|
@@ -54,6 +61,8 @@ function getCommonPathsWindows(name: string): string[] {
|
|
|
54
61
|
path.join(appData, "npm", `${name}.cmd`),
|
|
55
62
|
path.join(localAppData, "pnpm", `${name}.cmd`),
|
|
56
63
|
path.join(userProfile, ".local", "bin", `${name}.exe`),
|
|
64
|
+
// Native installers that keep a per-tool folder (Antigravity: %LOCALAPPDATA%\agy\bin).
|
|
65
|
+
path.join(localAppData, name, "bin", `${name}.exe`),
|
|
57
66
|
];
|
|
58
67
|
}
|
|
59
68
|
|
|
@@ -158,6 +167,7 @@ export async function findBinary(name: string, configOverride?: string): Promise
|
|
|
158
167
|
agent: "Install the Cursor CLI agent",
|
|
159
168
|
opencode: "Install: npm install -g opencode-ai",
|
|
160
169
|
pi: "Install: npm install -g @mariozechner/pi-coding-agent",
|
|
170
|
+
agy: "Install the Antigravity CLI: curl -fsSL https://antigravity.google/cli/install.sh | bash",
|
|
161
171
|
};
|
|
162
172
|
throw new Error(
|
|
163
173
|
`Could not find "${name}" binary. Searched common install paths and PATH. ` +
|
|
@@ -77,11 +77,12 @@ const RUNTIME_INSTRUCTIONS: Record<SkillRuntime, RuntimeInstructionSpec> = {
|
|
|
77
77
|
codex: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
78
78
|
opencode: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
79
79
|
gemini: { projectFile: "AGENTS.md", nativeFile: "GEMINI.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
80
|
+
antigravity: { projectFile: "AGENTS.md", nativeFile: "GEMINI.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
80
81
|
cursor: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: false },
|
|
81
82
|
pi: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
82
83
|
};
|
|
83
84
|
|
|
84
|
-
const ALL_RUNTIMES: SkillRuntime[] = ["claude", "codex", "gemini", "cursor", "opencode", "pi"];
|
|
85
|
+
const ALL_RUNTIMES: SkillRuntime[] = ["claude", "codex", "gemini", "antigravity", "cursor", "opencode", "pi"];
|
|
85
86
|
|
|
86
87
|
const DEFAULT_MANAGED_TAG = "agentex";
|
|
87
88
|
|
|
@@ -235,19 +236,19 @@ export function resolveInstructionTargets(options?: {
|
|
|
235
236
|
return [...byFile.values()].map((target) => ({ ...target, runtimes: dedupeRuntimes(target.runtimes) }));
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
// global: each runtime reads its own native file in its own home dir.
|
|
239
|
-
|
|
239
|
+
// global: each runtime reads its own native file in its own home dir. Gemini
|
|
240
|
+
// CLI and Antigravity share ~/.gemini/GEMINI.md, so targets merge by path.
|
|
241
|
+
const byPath = new Map<string, InstructionTarget>();
|
|
240
242
|
for (const runtime of runtimes) {
|
|
241
243
|
const spec = RUNTIME_INSTRUCTIONS[runtime];
|
|
242
244
|
if (!spec.hasGlobalFile) continue; // e.g. cursor global = app User Rules, not a file
|
|
243
245
|
const home = getDefaultRuntimeHome(runtime, options?.homeDir);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
246
|
+
const targetPath = path.join(home, spec.nativeFile);
|
|
247
|
+
const existing = byPath.get(targetPath);
|
|
248
|
+
if (existing) existing.runtimes.push(runtime);
|
|
249
|
+
else byPath.set(targetPath, { filename: spec.nativeFile, targetPath, runtimes: [runtime] });
|
|
249
250
|
}
|
|
250
|
-
return
|
|
251
|
+
return [...byPath.values()];
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
function dedupeRuntimes(runtimes: SkillRuntime[]): SkillRuntime[] {
|
|
@@ -10,6 +10,7 @@ const ENV_VAR_MAP: Record<SkillRuntime, string | null> = {
|
|
|
10
10
|
claude: "CLAUDE_CONFIG_DIR",
|
|
11
11
|
codex: "CODEX_HOME",
|
|
12
12
|
gemini: "GEMINI_CONFIG_DIR",
|
|
13
|
+
antigravity: null, // agy has no home override
|
|
13
14
|
cursor: "CURSOR_CONFIG_DIR",
|
|
14
15
|
opencode: "XDG_CONFIG_HOME", // affects ~/.config/opencode/
|
|
15
16
|
pi: "PI_HOME",
|
|
@@ -22,6 +23,9 @@ const HOME_SUBPATH_MAP: Record<SkillRuntime, string[]> = {
|
|
|
22
23
|
claude: [".claude"],
|
|
23
24
|
codex: [".codex"],
|
|
24
25
|
gemini: [".gemini"],
|
|
26
|
+
// Shared with Gemini CLI: agy reads its global GEMINI.md from ~/.gemini and
|
|
27
|
+
// keeps its own settings and skills in ~/.gemini/antigravity-cli.
|
|
28
|
+
antigravity: [".gemini"],
|
|
25
29
|
cursor: [".cursor"],
|
|
26
30
|
opencode: [".config", "opencode"],
|
|
27
31
|
pi: [".pi"],
|