dsh-agy-link 0.1.2

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/dist/index.js ADDED
@@ -0,0 +1,2285 @@
1
+ import { delimiter, dirname, join } from "node:path";
2
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
5
+ import { execFileSync, spawn } from "node:child_process";
6
+ import { defineTool } from "@deepseek-ai/dsh-tools";
7
+ //#region src/common/types.ts
8
+ const PROVIDER_ID = "antigravity";
9
+ const PLUGIN_ID = "agy-link";
10
+ const DEFAULT_FALLBACK_MODELS = [
11
+ {
12
+ id: "gemini-3-6-flash",
13
+ name: "Gemini 3.6 Flash",
14
+ efforts: [
15
+ "low",
16
+ "medium",
17
+ "high"
18
+ ]
19
+ },
20
+ {
21
+ id: "gemini-3-1-pro",
22
+ name: "Gemini 3.1 Pro",
23
+ efforts: ["low", "high"]
24
+ },
25
+ {
26
+ id: "claude-sonnet-4-6",
27
+ name: "Claude Sonnet 4.6 (Thinking)"
28
+ }
29
+ ];
30
+ function defaultConfig() {
31
+ return {
32
+ enabled: true,
33
+ agyBin: "",
34
+ extraArgs: [],
35
+ permissionMode: "skip",
36
+ defaultModel: "",
37
+ defaultEffort: "",
38
+ timeoutMs: 6e5,
39
+ maxConcurrent: 3,
40
+ contextWindowDefault: 1048576,
41
+ maxTokensDefault: 65536,
42
+ forwardSystemPrompt: false,
43
+ digestMaxChars: 8e3,
44
+ modelsCacheTtlMs: 3e5,
45
+ allowAuxiliary: true,
46
+ compactionMaxChars: 8e5,
47
+ workspaceRoot: "",
48
+ fallbackModels: DEFAULT_FALLBACK_MODELS,
49
+ askTool: false
50
+ };
51
+ }
52
+ const Err = {
53
+ AUTH: "AUTH",
54
+ AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
55
+ AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
56
+ AGY_ERROR: "AGY_ERROR",
57
+ TIMEOUT: "TIMEOUT",
58
+ PROCESS_EXIT: "PROCESS_EXIT",
59
+ INVALID_OUTPUT: "INVALID_OUTPUT",
60
+ UNKNOWN_MODEL: "UNKNOWN_MODEL",
61
+ UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
62
+ AUX_DISABLED: "AUX_DISABLED",
63
+ BUSY: "BUSY"
64
+ };
65
+ function looksLikeAuthFailure(text) {
66
+ return /authentication required|authentication failed|please sign in|not signed in|timed out waiting for authentication/i.test(text);
67
+ }
68
+ function extractAuthUrl(text) {
69
+ const m = text.match(/https:\/\/accounts\.google\.com\/\S+/);
70
+ if (!m) return void 0;
71
+ return m[0].replace(/[)\]>.,;\x27\x22]+$/, "");
72
+ }
73
+ //#endregion
74
+ //#region src/common/config.ts
75
+ function dshHome() {
76
+ return process.env.DSH_HOME ?? join(homedir(), ".dsh");
77
+ }
78
+ function stateDir() {
79
+ return join(dshHome(), "agy-link");
80
+ }
81
+ function overridesPath() {
82
+ return join(stateDir(), "runtime-overrides.json");
83
+ }
84
+ function readJson(file) {
85
+ try {
86
+ if (!existsSync(file)) return {};
87
+ const v = JSON.parse(readFileSync(file, "utf8"));
88
+ return v && typeof v === "object" ? v : {};
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+ function readOverrides(file = overridesPath()) {
94
+ return readJson(file);
95
+ }
96
+ function asString(v) {
97
+ return typeof v === "string" ? v : void 0;
98
+ }
99
+ function asBool(v) {
100
+ if (typeof v === "boolean") return v;
101
+ if (typeof v === "string") return v === "true" || v === "1";
102
+ }
103
+ function asNum(v) {
104
+ if (typeof v === "number" && Number.isFinite(v)) return v;
105
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
106
+ }
107
+ const MODES = [
108
+ "skip",
109
+ "plan",
110
+ "accept-edits"
111
+ ];
112
+ function asMode(v) {
113
+ return typeof v === "string" && MODES.includes(v) ? v : void 0;
114
+ }
115
+ /** Layered config read; cheap enough to call per request (thunk pattern). */
116
+ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
117
+ const base = defaultConfig();
118
+ const layers = [entry ?? {}, overrides];
119
+ const get = (k) => {
120
+ for (const l of layers) if (l[k] !== void 0 && l[k] !== null && l[k] !== "") return l[k];
121
+ };
122
+ const cfg = {
123
+ ...base,
124
+ enabled: asBool(get("enabled")) ?? base.enabled,
125
+ agyBin: asString(get("agyBin")) ?? base.agyBin,
126
+ extraArgs: Array.isArray(get("extraArgs")) ? get("extraArgs").filter((x) => typeof x === "string") : base.extraArgs,
127
+ permissionMode: asMode(get("permissionMode")) ?? base.permissionMode,
128
+ defaultModel: asString(get("defaultModel")) ?? base.defaultModel,
129
+ defaultEffort: asString(get("defaultEffort")) ?? base.defaultEffort,
130
+ timeoutMs: asNum(get("timeoutMs")) ?? base.timeoutMs,
131
+ maxConcurrent: asNum(get("maxConcurrent")) ?? base.maxConcurrent,
132
+ contextWindowDefault: asNum(get("contextWindowDefault")) ?? base.contextWindowDefault,
133
+ maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
134
+ forwardSystemPrompt: asBool(get("forwardSystemPrompt")) ?? base.forwardSystemPrompt,
135
+ digestMaxChars: asNum(get("digestMaxChars")) ?? base.digestMaxChars,
136
+ modelsCacheTtlMs: asNum(get("modelsCacheTtlMs")) ?? base.modelsCacheTtlMs,
137
+ allowAuxiliary: asBool(get("allowAuxiliary")) ?? base.allowAuxiliary,
138
+ compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
139
+ workspaceRoot: asString(get("workspaceRoot")) ?? base.workspaceRoot,
140
+ fallbackModels: Array.isArray(get("fallbackModels")) ? get("fallbackModels").filter((x) => !!x && typeof x === "object" && typeof x.id === "string") : base.fallbackModels,
141
+ askTool: asBool(get("askTool")) ?? base.askTool
142
+ };
143
+ if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
144
+ if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
145
+ if (env.DSH_AGY_MODE) {
146
+ const m = asMode(env.DSH_AGY_MODE);
147
+ if (m) cfg.permissionMode = m;
148
+ }
149
+ if (env.DSH_AGY_SKIP_PERMISSIONS !== void 0) {
150
+ const skip = asBool(env.DSH_AGY_SKIP_PERMISSIONS);
151
+ if (skip !== void 0) cfg.permissionMode = skip ? "skip" : "plan";
152
+ }
153
+ if (env.DSH_AGY_DEFAULT_MODEL) cfg.defaultModel = env.DSH_AGY_DEFAULT_MODEL;
154
+ if (env.DSH_AGY_DEFAULT_EFFORT) cfg.defaultEffort = env.DSH_AGY_DEFAULT_EFFORT;
155
+ if (env.DSH_AGY_TIMEOUT_MS) {
156
+ const t = asNum(env.DSH_AGY_TIMEOUT_MS);
157
+ if (t && t > 0) cfg.timeoutMs = t;
158
+ }
159
+ if (env.DSH_AGY_EXTRA_ARGS) cfg.extraArgs = env.DSH_AGY_EXTRA_ARGS.split(/\s+/).filter(Boolean);
160
+ return cfg;
161
+ }
162
+ //#endregion
163
+ //#region src/host/discovery.ts
164
+ function defaultConversationsDir() {
165
+ return process.env.DSH_AGY_CONVERSATIONS_DIR ?? join(homedir(), ".gemini", "antigravity-cli", "conversations");
166
+ }
167
+ function snapshotConversations(dir = defaultConversationsDir()) {
168
+ const files = /* @__PURE__ */ new Map();
169
+ try {
170
+ if (existsSync(dir)) for (const name of readdirSync(dir)) {
171
+ if (!name.endsWith(".db")) continue;
172
+ try {
173
+ files.set(name, statSync(join(dir, name)).mtimeMs);
174
+ } catch {}
175
+ }
176
+ } catch {}
177
+ return {
178
+ dir,
179
+ files
180
+ };
181
+ }
182
+ function diffConversations(before, dir = defaultConversationsDir()) {
183
+ const after = snapshotConversations(dir);
184
+ const added = [];
185
+ for (const [name, mtime] of after.files) if (!before.files.has(name)) added.push({
186
+ name,
187
+ mtime
188
+ });
189
+ if (added.length === 0) return {
190
+ conversationId: null,
191
+ ambiguous: false
192
+ };
193
+ added.sort((a, b) => b.mtime - a.mtime);
194
+ const top = added[0];
195
+ return {
196
+ conversationId: top !== void 0 ? top.name.replace(/\.db$/, "") : "",
197
+ ambiguous: added.length > 1
198
+ };
199
+ }
200
+ //#endregion
201
+ //#region src/host/mapper.ts
202
+ function briefArgs(args) {
203
+ if (args === void 0 || args === null) return "";
204
+ try {
205
+ const s = typeof args === "string" ? args : JSON.stringify(args);
206
+ return s.length > 300 ? s.slice(0, 300) + "..." : s;
207
+ } catch {
208
+ return String(args).slice(0, 120);
209
+ }
210
+ }
211
+ function defaultAnnounce(name, args) {
212
+ const brief = briefArgs(args);
213
+ return brief === "" ? "[agy tool: " + name + "]\n" : "[agy tool: " + name + "] " + brief + "\n";
214
+ }
215
+ function defaultOutput(output) {
216
+ if (output === void 0 || output === null) return null;
217
+ let s;
218
+ try {
219
+ s = typeof output === "string" ? output : JSON.stringify(output);
220
+ } catch {
221
+ s = String(output);
222
+ }
223
+ if (s === "") return null;
224
+ return "-> " + (s.length > 2048 ? s.slice(0, 2048) + "... (+" + (s.length - 2048) + " chars)" : s) + "\n";
225
+ }
226
+ function usageFromRaw(raw) {
227
+ return {
228
+ inputTokens: raw.input_tokens ?? 0,
229
+ outputTokens: raw.output_tokens ?? 0,
230
+ cacheReadTokens: raw.cache_read_tokens ?? void 0,
231
+ cacheWriteTokens: raw.cache_write_tokens ?? void 0,
232
+ reasoningTokens: raw.thinking_tokens ?? void 0
233
+ };
234
+ }
235
+ /** Suffix-delta: emit only what grew; fall back to a newline + full text. */
236
+ function suffixDelta(prev, next) {
237
+ if (next === prev) return "";
238
+ if (prev === "") return next;
239
+ if (next.startsWith(prev)) return next.slice(prev.length);
240
+ return "\n" + next;
241
+ }
242
+ var EventMapper = class {
243
+ opts;
244
+ blockIdx = 0;
245
+ openType = null;
246
+ openAcc = "";
247
+ emittedByKey = /* @__PURE__ */ new Map();
248
+ toolPhaseSeen = /* @__PURE__ */ new Set();
249
+ sawTextStep = false;
250
+ finished = false;
251
+ constructor(opts = {}) {
252
+ this.opts = opts;
253
+ }
254
+ /** Whether a terminal finish chunk has been emitted. */
255
+ get isFinished() {
256
+ return this.finished;
257
+ }
258
+ *ensureBlock(type) {
259
+ if (this.openType === type) return;
260
+ const close = this.closeOpen();
261
+ if (close) yield close;
262
+ this.openType = type;
263
+ this.openAcc = "";
264
+ yield {
265
+ type: "block-start",
266
+ index: this.blockIdx,
267
+ blockType: type
268
+ };
269
+ }
270
+ closeOpen() {
271
+ if (this.openType === null) return null;
272
+ const block = this.openType === "text" ? {
273
+ type: "text",
274
+ text: this.openAcc
275
+ } : {
276
+ type: "reasoning",
277
+ text: this.openAcc
278
+ };
279
+ const chunk = {
280
+ type: "block-end",
281
+ index: this.blockIdx,
282
+ block
283
+ };
284
+ this.blockIdx++;
285
+ this.openType = null;
286
+ this.openAcc = "";
287
+ return chunk;
288
+ }
289
+ appendDelta(delta) {
290
+ if (delta === "") return null;
291
+ this.openAcc += delta;
292
+ return this.openType === "text" ? {
293
+ type: "text-delta",
294
+ index: this.blockIdx,
295
+ text: delta
296
+ } : {
297
+ type: "reasoning-delta",
298
+ index: this.blockIdx,
299
+ text: delta
300
+ };
301
+ }
302
+ *map(ev) {
303
+ if (this.finished) return;
304
+ if (ev.kind === "init") return;
305
+ if (ev.kind === "garbage") return;
306
+ if (ev.kind === "step") {
307
+ if (ev.stepKind === "text") {
308
+ this.sawTextStep = true;
309
+ yield* this.ensureBlock("text");
310
+ const delta = suffixDelta(this.emittedByKey.get(ev.stepKey) ?? "", ev.text);
311
+ this.emittedByKey.set(ev.stepKey, ev.text);
312
+ const d = this.appendDelta(delta);
313
+ if (d) yield d;
314
+ return;
315
+ }
316
+ if (ev.stepKind === "thinking" || ev.stepKind === "tool" || ev.stepKind === "subagent") {
317
+ yield* this.ensureBlock("reasoning");
318
+ if (ev.stepKind === "thinking") {
319
+ const delta = suffixDelta(this.emittedByKey.get(ev.stepKey) ?? "", ev.text);
320
+ this.emittedByKey.set(ev.stepKey, ev.text);
321
+ const d = this.appendDelta(delta);
322
+ if (d) yield d;
323
+ } else if (ev.stepKind === "tool" && ev.tool) {
324
+ const announceKey = ev.stepKey + ":a";
325
+ if (!this.toolPhaseSeen.has(announceKey)) {
326
+ this.toolPhaseSeen.add(announceKey);
327
+ const line = (this.opts.announce ?? defaultAnnounce)(ev.tool.name, ev.tool.args);
328
+ const d = this.appendDelta(line);
329
+ if (d) yield d;
330
+ }
331
+ const outKey = ev.stepKey + ":o";
332
+ if (ev.tool.output !== void 0 && !this.toolPhaseSeen.has(outKey)) {
333
+ this.toolPhaseSeen.add(outKey);
334
+ const rendered = this.opts.toolOutput ? this.opts.toolOutput(ev.tool.name, ev.tool.args, ev.tool.output) : defaultOutput(ev.tool.output);
335
+ if (rendered !== null) {
336
+ const d = this.appendDelta(rendered);
337
+ if (d) yield d;
338
+ }
339
+ }
340
+ } else if (ev.stepKind === "subagent") {
341
+ const d = this.appendDelta("[agy subagent] " + ev.text + "\n");
342
+ if (d) yield d;
343
+ }
344
+ return;
345
+ }
346
+ return;
347
+ }
348
+ if (!ev.ok) return;
349
+ if (!this.sawTextStep && ev.response !== "") {
350
+ yield* this.ensureBlock("text");
351
+ const d = this.appendDelta(ev.response);
352
+ if (d) yield d;
353
+ }
354
+ const close = this.closeOpen();
355
+ if (close) yield close;
356
+ yield {
357
+ type: "usage",
358
+ usage: usageFromRaw(ev.usage)
359
+ };
360
+ yield {
361
+ type: "finish",
362
+ reason: { kind: "stop" },
363
+ replayState: ev.conversationId !== "" ? { response: { conversationId: ev.conversationId } } : void 0
364
+ };
365
+ this.finished = true;
366
+ }
367
+ /** Terminal error/abort: close what is open, zero usage, failure finish. */
368
+ *emitFailure(kind, code, message) {
369
+ if (this.finished) return;
370
+ const close = this.closeOpen();
371
+ if (close) yield close;
372
+ yield {
373
+ type: "usage",
374
+ usage: {
375
+ inputTokens: 0,
376
+ outputTokens: 0
377
+ }
378
+ };
379
+ yield {
380
+ type: "finish",
381
+ reason: kind === "error" ? {
382
+ kind: "error",
383
+ failure: {
384
+ message,
385
+ code
386
+ }
387
+ } : {
388
+ kind: "aborted",
389
+ failure: {
390
+ message,
391
+ code
392
+ }
393
+ }
394
+ };
395
+ this.finished = true;
396
+ }
397
+ };
398
+ //#endregion
399
+ //#region src/host/models.ts
400
+ /** Parse `agy models` stdout: JSON shapes first, then two-column text. */
401
+ function parseModelsOutput(stdout) {
402
+ const text = stdout.trim();
403
+ if (text === "") return [];
404
+ try {
405
+ const list = extractModelList(JSON.parse(text));
406
+ if (list) return list;
407
+ } catch {}
408
+ const out = [];
409
+ for (const line of text.split(/\n/)) {
410
+ const t = line.trim();
411
+ if (t === "" || t.startsWith("Fetching") || t.startsWith("Error")) continue;
412
+ const m = t.match(/^(\S+)\s{2,}(.+)$/);
413
+ if (m && m[1] !== void 0 && m[2] !== void 0) out.push({
414
+ slug: m[1],
415
+ label: m[2].trim()
416
+ });
417
+ else if (/^\S+$/.test(t)) out.push({
418
+ slug: t,
419
+ label: t
420
+ });
421
+ }
422
+ return out;
423
+ }
424
+ function extractModelList(parsed) {
425
+ let arr = null;
426
+ if (Array.isArray(parsed)) arr = parsed;
427
+ else if (parsed && typeof parsed === "object") {
428
+ const o = parsed;
429
+ for (const k of [
430
+ "models",
431
+ "items",
432
+ "data",
433
+ "result"
434
+ ]) if (Array.isArray(o[k])) {
435
+ arr = o[k];
436
+ break;
437
+ }
438
+ }
439
+ if (!arr) return null;
440
+ const out = [];
441
+ for (const item of arr) {
442
+ if (typeof item === "string") {
443
+ out.push({
444
+ slug: item,
445
+ label: item
446
+ });
447
+ continue;
448
+ }
449
+ if (!item || typeof item !== "object") continue;
450
+ const o = item;
451
+ const slugV = o.slug ?? o.id ?? o.name ?? o.model;
452
+ const labelV = o.label ?? o.display_name ?? o.displayName ?? o.title ?? slugV;
453
+ if (typeof slugV === "string" && slugV !== "") out.push({
454
+ slug: slugV,
455
+ label: typeof labelV === "string" ? labelV : slugV
456
+ });
457
+ }
458
+ return out;
459
+ }
460
+ const EFFORT_SUFFIXES = [
461
+ "low",
462
+ "medium",
463
+ "high"
464
+ ];
465
+ /** Fold Gemini effort variants into base + effort set (spec ADR-10). */
466
+ function foldEfforts(raw) {
467
+ const bases = /* @__PURE__ */ new Map();
468
+ const verbatim = [];
469
+ const slugSet = new Set(raw.map((r) => r.slug));
470
+ for (const r of raw) {
471
+ if (!r.slug.startsWith("gemini")) {
472
+ verbatim.push({
473
+ id: r.slug,
474
+ name: r.label,
475
+ efforts: null
476
+ });
477
+ continue;
478
+ }
479
+ let folded = false;
480
+ for (const eff of EFFORT_SUFFIXES) {
481
+ const suffix = "-" + eff;
482
+ if (r.slug.endsWith(suffix)) {
483
+ const base = r.slug.slice(0, -suffix.length);
484
+ const hasBare = slugSet.has(base);
485
+ const hasSibling = raw.some((x) => x.slug.startsWith(base + "-") && EFFORT_SUFFIXES.some((e) => x.slug.endsWith("-" + e)) && x.slug !== r.slug);
486
+ if (hasBare || hasSibling) {
487
+ const entry = bases.get(base) ?? {
488
+ label: stripEffortLabel(r.label, eff),
489
+ efforts: /* @__PURE__ */ new Set()
490
+ };
491
+ entry.efforts.add(eff);
492
+ bases.set(base, entry);
493
+ folded = true;
494
+ break;
495
+ }
496
+ }
497
+ }
498
+ if (!folded) verbatim.push({
499
+ id: r.slug,
500
+ name: r.label,
501
+ efforts: null
502
+ });
503
+ }
504
+ const folded = [];
505
+ for (const [id, v] of bases) {
506
+ const efforts = EFFORT_SUFFIXES.filter((e) => v.efforts.has(e));
507
+ folded.push({
508
+ id,
509
+ name: v.label !== "" ? v.label : id,
510
+ efforts: efforts.length > 0 ? efforts : null
511
+ });
512
+ }
513
+ const rawOrder = new Map(raw.map((r, i) => [r.slug, i]));
514
+ const rank = (e) => {
515
+ let best = Infinity;
516
+ for (const r of raw) if (r.slug === e.id || r.slug.startsWith(e.id + "-")) best = Math.min(best, rawOrder.get(r.slug) ?? Infinity);
517
+ return best;
518
+ };
519
+ folded.sort((a, b) => rank(a) - rank(b));
520
+ verbatim.sort((a, b) => rank(a) - rank(b));
521
+ return [...folded, ...verbatim];
522
+ }
523
+ function stripEffortLabel(label, eff) {
524
+ const re = new RegExp("\\s*\\(?" + eff + "\\)?\\s*$", "i");
525
+ return label.replace(re, "").trim();
526
+ }
527
+ function buildFallbackCatalog(defs) {
528
+ return defs.map((d) => ({
529
+ id: d.id,
530
+ name: d.name,
531
+ efforts: d.efforts ?? null
532
+ }));
533
+ }
534
+ var ModelCatalog = class {
535
+ discover;
536
+ ttlMs;
537
+ current;
538
+ refreshing = null;
539
+ constructor(discover, fallbackDefs, ttlMs) {
540
+ this.discover = discover;
541
+ this.ttlMs = ttlMs;
542
+ this.current = {
543
+ source: "fallback",
544
+ models: buildFallbackCatalog(fallbackDefs),
545
+ discoveredAt: 0
546
+ };
547
+ }
548
+ get() {
549
+ return this.current;
550
+ }
551
+ /** Refresh if stale; never throws — failures keep the previous catalog. */
552
+ async refreshIfNeeded() {
553
+ if (this.refreshing) return this.refreshing;
554
+ const age = Date.now() - this.current.discoveredAt;
555
+ if (this.current.source === "discovered" && age < this.ttlMs) return;
556
+ this.refreshing = this.refresh().finally(() => {
557
+ this.refreshing = null;
558
+ });
559
+ return this.refreshing;
560
+ }
561
+ async forceRefresh() {
562
+ await this.refresh();
563
+ return this.current;
564
+ }
565
+ async refresh() {
566
+ try {
567
+ const ac = new AbortController();
568
+ const timer = setTimeout(() => ac.abort(), 3e4);
569
+ try {
570
+ const { stdout } = await this.discover(ac.signal);
571
+ const raw = parseModelsOutput(stdout);
572
+ if (raw.length > 0) {
573
+ this.current = {
574
+ source: "discovered",
575
+ models: foldEfforts(raw),
576
+ discoveredAt: Date.now()
577
+ };
578
+ return;
579
+ }
580
+ this.current = {
581
+ ...this.current,
582
+ lastError: "agy models returned no entries"
583
+ };
584
+ } finally {
585
+ clearTimeout(timer);
586
+ }
587
+ } catch (err) {
588
+ this.current = {
589
+ ...this.current,
590
+ lastError: err instanceof Error ? err.message : String(err)
591
+ };
592
+ }
593
+ }
594
+ };
595
+ function findEntry(catalog, id) {
596
+ return catalog.models.find((m) => m.id === id);
597
+ }
598
+ function defaultEffortFor(entry, cfg) {
599
+ if (!entry.efforts) return void 0;
600
+ if (cfg.defaultEffort !== "" && entry.efforts.includes(cfg.defaultEffort)) return cfg.defaultEffort;
601
+ return entry.efforts.includes("medium") ? "medium" : entry.efforts[entry.efforts.length - 1];
602
+ }
603
+ //#endregion
604
+ //#region src/host/parser.ts
605
+ const TEXT_STEP_TYPES = /* @__PURE__ */ new Set([
606
+ "agent_text",
607
+ "agenttext",
608
+ "text",
609
+ "model_response",
610
+ "modelresponse",
611
+ "message",
612
+ "response_text"
613
+ ]);
614
+ const THINKING_STEP_TYPES = /* @__PURE__ */ new Set([
615
+ "thinking",
616
+ "thought",
617
+ "reasoning"
618
+ ]);
619
+ const TOOL_STEP_TYPES = /* @__PURE__ */ new Set([
620
+ "tool_call",
621
+ "toolcall",
622
+ "tool",
623
+ "tool_use",
624
+ "tooluse",
625
+ "tool_run",
626
+ "toolrun",
627
+ "function_call"
628
+ ]);
629
+ const TITLE_STEP_TYPES = /* @__PURE__ */ new Set(["title"]);
630
+ const SUBAGENT_STEP_TYPES = /* @__PURE__ */ new Set([
631
+ "subagent",
632
+ "subagent_message",
633
+ "subagent_result"
634
+ ]);
635
+ const USER_INPUT_STEP_TYPES = /* @__PURE__ */ new Set([
636
+ "user_input",
637
+ "userinput",
638
+ "user_message"
639
+ ]);
640
+ const NUMERIC_STEP_TYPES = {
641
+ 14: "thinking",
642
+ 15: "text",
643
+ 23: "title",
644
+ 5: "tool",
645
+ 7: "tool",
646
+ 8: "tool",
647
+ 9: "tool",
648
+ 17: "tool",
649
+ 21: "tool",
650
+ 33: "tool",
651
+ 101: "tool",
652
+ 132: "tool",
653
+ 138: "tool",
654
+ 139: "tool"
655
+ };
656
+ function pick(obj, keys) {
657
+ for (const k of keys) {
658
+ const v = obj[k];
659
+ if (v !== void 0 && v !== null) return v;
660
+ }
661
+ }
662
+ function normalizeStepKind(v) {
663
+ if (typeof v === "number") return NUMERIC_STEP_TYPES[v] ?? "unknown";
664
+ if (typeof v === "string") {
665
+ const s = v.toLowerCase();
666
+ if (TEXT_STEP_TYPES.has(s)) return "text";
667
+ if (THINKING_STEP_TYPES.has(s)) return "thinking";
668
+ if (TOOL_STEP_TYPES.has(s)) return "tool";
669
+ if (TITLE_STEP_TYPES.has(s)) return "title";
670
+ if (SUBAGENT_STEP_TYPES.has(s)) return "subagent";
671
+ if (USER_INPUT_STEP_TYPES.has(s)) return "user-input";
672
+ }
673
+ return "unknown";
674
+ }
675
+ function extractText(obj) {
676
+ const direct = pick(obj, [
677
+ "text",
678
+ "content",
679
+ "agent_text",
680
+ "agentText",
681
+ "output_text",
682
+ "payload_text"
683
+ ]);
684
+ if (typeof direct === "string") return direct;
685
+ const payload = obj.payload ?? obj.step_payload ?? obj.stepPayload;
686
+ if (payload && typeof payload === "object") {
687
+ const inner = pick(payload, [
688
+ "text",
689
+ "content",
690
+ "agent_text",
691
+ "agentText"
692
+ ]);
693
+ if (typeof inner === "string") return inner;
694
+ }
695
+ return "";
696
+ }
697
+ function extractTool(obj) {
698
+ const info = pick(obj, [
699
+ "tool_info",
700
+ "toolInfo",
701
+ "tool",
702
+ "tool_call",
703
+ "toolCall"
704
+ ]);
705
+ const src = info && typeof info === "object" ? info : obj;
706
+ const name = pick(src, [
707
+ "name",
708
+ "tool_name",
709
+ "toolName",
710
+ "canonical_name"
711
+ ]);
712
+ if (typeof name !== "string" || name === "") return void 0;
713
+ return {
714
+ name,
715
+ args: pick(src, [
716
+ "parameters",
717
+ "params",
718
+ "input",
719
+ "args",
720
+ "input_json",
721
+ "inputJson"
722
+ ]),
723
+ output: pick(src, [
724
+ "output",
725
+ "result",
726
+ "output_text"
727
+ ])
728
+ };
729
+ }
730
+ function parseUsage(v) {
731
+ if (!v || typeof v !== "object") return {};
732
+ const o = v;
733
+ const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : void 0;
734
+ return {
735
+ input_tokens: num(o.input_tokens),
736
+ output_tokens: num(o.output_tokens),
737
+ thinking_tokens: num(o.thinking_tokens),
738
+ cache_read_tokens: num(o.cache_read_tokens),
739
+ cache_write_tokens: num(o.cache_write_tokens),
740
+ total_tokens: num(o.total_tokens)
741
+ };
742
+ }
743
+ /** Parse one decoded JSON object into a typed event; undefined = ignore. */
744
+ function classifyEvent(obj, seq) {
745
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return void 0;
746
+ const o = obj;
747
+ const evt = typeof o.event === "string" ? o.event : typeof o.type === "string" ? o.type : "";
748
+ if (evt === "init" || evt === "initialized") {
749
+ const cid = pick(o, [
750
+ "conversation_id",
751
+ "conversationId",
752
+ "session_id",
753
+ "sessionId"
754
+ ]);
755
+ const model = pick(o, [
756
+ "model",
757
+ "model_name",
758
+ "modelName"
759
+ ]);
760
+ return {
761
+ kind: "init",
762
+ conversationId: typeof cid === "string" && cid !== "" ? cid : void 0,
763
+ model: typeof model === "string" ? model : void 0,
764
+ raw: o
765
+ };
766
+ }
767
+ if (evt === "step_update" || evt === "step" || evt === "stepUpdate") {
768
+ const idxV = pick(o, [
769
+ "idx",
770
+ "index",
771
+ "step_idx",
772
+ "stepIdx",
773
+ "id",
774
+ "step_id",
775
+ "stepId"
776
+ ]);
777
+ const stepKey = typeof idxV === "number" || typeof idxV === "string" ? String(idxV) : String(seq);
778
+ const stepKind = normalizeStepKind(pick(o, [
779
+ "step_type",
780
+ "stepType",
781
+ "type"
782
+ ]));
783
+ return {
784
+ kind: "step",
785
+ stepKey,
786
+ stepKind,
787
+ text: extractText(o),
788
+ tool: stepKind === "tool" ? extractTool(o) : void 0,
789
+ raw: o
790
+ };
791
+ }
792
+ if (evt === "result" || evt === "done" || evt === "final") {
793
+ const r = o.result ?? o;
794
+ const inner = r && typeof r === "object" ? r : {};
795
+ const cid = pick(inner, ["conversation_id", "conversationId"]);
796
+ const status = pick(inner, ["status"]);
797
+ const response = pick(inner, [
798
+ "response",
799
+ "text",
800
+ "content"
801
+ ]);
802
+ const error = pick(inner, [
803
+ "error",
804
+ "error_message",
805
+ "errorMessage"
806
+ ]);
807
+ const ok = status === void 0 ? !error : String(status).toUpperCase() !== "ERROR";
808
+ return {
809
+ kind: "result",
810
+ conversationId: typeof cid === "string" ? cid : "",
811
+ ok,
812
+ response: typeof response === "string" ? response : "",
813
+ error: typeof error === "string" ? error : void 0,
814
+ usage: parseUsage(inner.usage),
815
+ raw: o
816
+ };
817
+ }
818
+ if (o.usage && typeof o.usage === "object" && (o.status !== void 0 || o.response !== void 0)) return classifyEvent({
819
+ event: "result",
820
+ result: o
821
+ }, seq);
822
+ if (o.step_type !== void 0 || o.stepType !== void 0) return classifyEvent({
823
+ event: "step_update",
824
+ ...o
825
+ }, seq);
826
+ }
827
+ var StreamJsonParser = class {
828
+ buffer = "";
829
+ seq = 0;
830
+ stats = {
831
+ lines: 0,
832
+ garbage: 0,
833
+ consecutiveGarbage: 0,
834
+ authUrl: void 0,
835
+ sawAuthFailure: false
836
+ };
837
+ /** Ring buffer of raw stdout lines for /agy doctor export. */
838
+ recentLines = [];
839
+ maxRecent = 2e3;
840
+ /** Feed a stdout chunk; returns the events completed by it. */
841
+ feed(chunk) {
842
+ this.buffer += chunk;
843
+ const out = [];
844
+ let nl;
845
+ while ((nl = this.buffer.indexOf("\n")) >= 0) {
846
+ const line = this.buffer.slice(0, nl).replace(/\r$/, "");
847
+ this.buffer = this.buffer.slice(nl + 1);
848
+ const ev = this.takeLine(line);
849
+ if (ev) out.push(ev);
850
+ }
851
+ return out;
852
+ }
853
+ /** Flush a trailing line without a newline (agy killed mid-write). */
854
+ flush() {
855
+ const rest = this.buffer;
856
+ this.buffer = "";
857
+ if (rest.trim() === "") return [];
858
+ const ev = this.takeLine(rest);
859
+ return ev ? [ev] : [];
860
+ }
861
+ takeLine(line) {
862
+ if (line.trim() === "") return void 0;
863
+ this.stats.lines++;
864
+ this.recentLines.push(line);
865
+ if (this.recentLines.length > this.maxRecent) this.recentLines.splice(0, this.recentLines.length - this.maxRecent);
866
+ if (looksLikeAuthFailure(line)) this.stats.sawAuthFailure = true;
867
+ if (this.stats.authUrl === void 0) {
868
+ const u = extractAuthUrl(line);
869
+ if (u) this.stats.authUrl = u;
870
+ }
871
+ let obj;
872
+ try {
873
+ obj = JSON.parse(line);
874
+ } catch {
875
+ this.stats.garbage++;
876
+ this.stats.consecutiveGarbage++;
877
+ return {
878
+ kind: "garbage",
879
+ line
880
+ };
881
+ }
882
+ const ev = classifyEvent(obj, this.seq++);
883
+ if (!ev) {
884
+ this.stats.garbage++;
885
+ this.stats.consecutiveGarbage++;
886
+ return {
887
+ kind: "garbage",
888
+ line
889
+ };
890
+ }
891
+ this.stats.consecutiveGarbage = 0;
892
+ return ev;
893
+ }
894
+ };
895
+ function resolveAgyBin(cfg) {
896
+ const candidates = [];
897
+ if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
898
+ const pathEnv = process.env.PATH ?? "";
899
+ for (const dir of pathEnv.split(delimiter)) if (dir !== "") candidates.push(join(dir, "agy"));
900
+ candidates.push(join(homedir(), ".local", "bin", "agy"));
901
+ candidates.push("/usr/local/bin/agy");
902
+ for (const c of candidates) try {
903
+ accessSync(c, constants.X_OK);
904
+ return c;
905
+ } catch {
906
+ continue;
907
+ }
908
+ return null;
909
+ }
910
+ function compareVersions(a, b) {
911
+ const pa = a.split(/\./).map(Number);
912
+ const pb = b.split(/\./).map(Number);
913
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
914
+ const x = pa[i] ?? 0;
915
+ const y = pb[i] ?? 0;
916
+ if (x !== y) return x - y;
917
+ }
918
+ return 0;
919
+ }
920
+ function parseVersion(out) {
921
+ return out.match(/(\d+\.\d+\.\d+)/)?.[1] ?? null;
922
+ }
923
+ function killTree(child) {
924
+ if (child.pid === void 0) return;
925
+ try {
926
+ process.kill(-child.pid, "SIGTERM");
927
+ } catch {
928
+ try {
929
+ child.kill("SIGTERM");
930
+ } catch {}
931
+ }
932
+ }
933
+ function startAgyProcess(opts) {
934
+ const started = Date.now();
935
+ const child = spawn(opts.bin, opts.args, {
936
+ cwd: opts.cwd,
937
+ env: process.env,
938
+ detached: true,
939
+ stdio: [
940
+ "pipe",
941
+ "pipe",
942
+ "pipe"
943
+ ]
944
+ });
945
+ let stdout = "";
946
+ let stderr = "";
947
+ let timedOut = false;
948
+ let aborted = false;
949
+ let settled = false;
950
+ const watchdog = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
951
+ timedOut = true;
952
+ killTree(child);
953
+ }, opts.timeoutMs) : null;
954
+ const onAbort = () => {
955
+ aborted = true;
956
+ killTree(child);
957
+ };
958
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
959
+ if (child.stdout) child.stdout.setEncoding("utf8");
960
+ if (child.stderr) child.stderr.setEncoding("utf8");
961
+ let pending = "";
962
+ child.stdout?.on("data", (chunk) => {
963
+ stdout += chunk;
964
+ if (stdout.length > 4e6) stdout = stdout.slice(-2e6);
965
+ pending += chunk;
966
+ let nl;
967
+ while ((nl = pending.indexOf("\n")) >= 0) {
968
+ const line = pending.slice(0, nl);
969
+ pending = pending.slice(nl + 1);
970
+ opts.onLine?.(line);
971
+ }
972
+ });
973
+ child.stderr?.on("data", (chunk) => {
974
+ stderr = (stderr + chunk).slice(-4096);
975
+ });
976
+ return {
977
+ child,
978
+ outcome: new Promise((resolve) => {
979
+ const finish = (code, signal) => {
980
+ if (settled) return;
981
+ settled = true;
982
+ if (watchdog) clearTimeout(watchdog);
983
+ opts.signal?.removeEventListener("abort", onAbort);
984
+ if (pending !== "") {
985
+ opts.onLine?.(pending);
986
+ pending = "";
987
+ }
988
+ if (!opts.keepStdin) try {
989
+ child.stdin?.end();
990
+ } catch {}
991
+ resolve({
992
+ code,
993
+ signal,
994
+ timedOut,
995
+ aborted,
996
+ stdout,
997
+ stderrTail: stderr,
998
+ durationMs: Date.now() - started
999
+ });
1000
+ };
1001
+ child.on("exit", (code, signal) => finish(code, signal));
1002
+ child.on("error", (err) => {
1003
+ stderr = (stderr + String(err)).slice(-4096);
1004
+ finish(null, null);
1005
+ });
1006
+ }),
1007
+ kill: (reason) => {
1008
+ if (reason === "timeout") timedOut = true;
1009
+ else aborted = true;
1010
+ killTree(child);
1011
+ }
1012
+ };
1013
+ }
1014
+ /** Simple one-shot helper for --version / models probes. */
1015
+ async function probeProcess(bin, args, timeoutMs = 3e4, signal) {
1016
+ return startAgyProcess({
1017
+ bin,
1018
+ args,
1019
+ timeoutMs,
1020
+ signal
1021
+ }).outcome;
1022
+ }
1023
+ //#endregion
1024
+ //#region src/host/adapter.ts
1025
+ function textOf(m) {
1026
+ const parts = [];
1027
+ for (const b of m.content) if (b.type === "text") parts.push(b.text);
1028
+ return parts.filter((s) => s !== "").join("\n");
1029
+ }
1030
+ function isForeignAssistant(m) {
1031
+ if (m.role !== "assistant") return false;
1032
+ const src = m.source;
1033
+ return !src || src.provider !== "antigravity";
1034
+ }
1035
+ /** Rolling digest of turns this agy conversation has not seen (ADR-7). */
1036
+ function buildDigest(messages, fromIdx, maxChars) {
1037
+ const parts = [];
1038
+ let budget = maxChars;
1039
+ for (let i = messages.length - 1; i >= fromIdx; i--) {
1040
+ const m = messages[i];
1041
+ if (m === void 0 || m.role === "system") continue;
1042
+ const text = textOf(m);
1043
+ if (text === "") continue;
1044
+ const line = (m.role === "user" ? "User: " : "Assistant: ") + text;
1045
+ if (line.length > budget) {
1046
+ parts.unshift(line.slice(0, Math.max(0, budget)));
1047
+ break;
1048
+ }
1049
+ budget -= line.length;
1050
+ parts.unshift(line);
1051
+ }
1052
+ if (parts.length === 0) return "";
1053
+ return "[conversation so far]\n" + parts.join("\n\n") + "\n[end of conversation so far]\n\n";
1054
+ }
1055
+ var ChunkQueue = class {
1056
+ chunks = [];
1057
+ wake = null;
1058
+ closed = false;
1059
+ push(ch) {
1060
+ this.chunks.push(ch);
1061
+ this.wake?.();
1062
+ this.wake = null;
1063
+ }
1064
+ close() {
1065
+ this.closed = true;
1066
+ this.wake?.();
1067
+ this.wake = null;
1068
+ }
1069
+ async *drain() {
1070
+ for (;;) {
1071
+ while (this.chunks.length > 0) {
1072
+ const ch = this.chunks.shift();
1073
+ if (ch !== void 0) yield ch;
1074
+ }
1075
+ if (this.closed) return;
1076
+ await new Promise((resolve) => {
1077
+ this.wake = resolve;
1078
+ });
1079
+ }
1080
+ }
1081
+ };
1082
+ function brief(s) {
1083
+ const flat = s.trim().replace(/\s+/g, " ");
1084
+ return flat.length > 300 ? flat.slice(0, 300) + "..." : flat;
1085
+ }
1086
+ function sawAuthFailure(parser, outcome) {
1087
+ if (parser.stats.sawAuthFailure) return true;
1088
+ return looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3));
1089
+ }
1090
+ var AgyAdapter = class extends LlmAdapter {
1091
+ deps;
1092
+ constructor(deps) {
1093
+ super();
1094
+ this.deps = deps;
1095
+ }
1096
+ /**
1097
+ * Fail fast on auth and abort; allow one retry for transient process
1098
+ * failures (timeout, crash, malformed stream) per ADR-11.
1099
+ */
1100
+ providerRetryPolicy(_provider) {
1101
+ return {
1102
+ mode: "normal",
1103
+ maxRetries: 1,
1104
+ retryableCodes: [
1105
+ Err.TIMEOUT,
1106
+ Err.PROCESS_EXIT,
1107
+ Err.INVALID_OUTPUT
1108
+ ],
1109
+ initialDelayMs: 2e3,
1110
+ maxDelayMs: 1e4,
1111
+ jitterRatio: .1
1112
+ };
1113
+ }
1114
+ providerInfo(_provider) {
1115
+ return {
1116
+ id: PROVIDER_ID,
1117
+ name: "Antigravity (agy CLI)"
1118
+ };
1119
+ }
1120
+ async listModels(_provider) {
1121
+ this.deps.catalog.refreshIfNeeded();
1122
+ return this.deps.catalog.get().models.map((m) => ({
1123
+ provider: PROVIDER_ID,
1124
+ id: m.id,
1125
+ name: m.name,
1126
+ inputModalities: ["text"]
1127
+ }));
1128
+ }
1129
+ async resolveModel(_provider, model, _signal) {
1130
+ const cfg = this.deps.getConfig();
1131
+ const entry = findEntry(this.deps.catalog.get(), model);
1132
+ const name = entry ? entry.name : model;
1133
+ const resolved = {
1134
+ provider: PROVIDER_ID,
1135
+ id: model,
1136
+ name,
1137
+ inputModalities: ["text"],
1138
+ context: { contextWindow: cfg.contextWindowDefault },
1139
+ defaultMaxTokens: cfg.maxTokensDefault
1140
+ };
1141
+ if (entry && entry.efforts) {
1142
+ const def = defaultEffortFor(entry, cfg);
1143
+ resolved.reasoning = {
1144
+ efforts: entry.efforts.map((e) => ({
1145
+ id: e,
1146
+ name: e
1147
+ })),
1148
+ ...def ? { defaultEffort: def } : {}
1149
+ };
1150
+ }
1151
+ return resolved;
1152
+ }
1153
+ /** Build the agy argv for one call. Exported for tests. */
1154
+ buildArgs(opts) {
1155
+ const args = [
1156
+ "--output-format",
1157
+ "stream-json",
1158
+ "--print-timeout",
1159
+ Math.max(1, Math.ceil(opts.timeoutMs / 6e4)) + "m"
1160
+ ];
1161
+ if (opts.permissionMode === "skip") args.push("--dangerously-skip-permissions");
1162
+ else args.push("--mode", opts.permissionMode);
1163
+ if (opts.model !== "") args.push("--model", opts.model);
1164
+ if (opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
1165
+ if (opts.conversationId) args.push("--conversation", opts.conversationId);
1166
+ args.push(...opts.extraArgs);
1167
+ args.push("-p", opts.prompt);
1168
+ return args;
1169
+ }
1170
+ async *stream(options) {
1171
+ const cfg = this.deps.getConfig();
1172
+ const bin = this.deps.bin();
1173
+ if (!bin) throw new LlmError("agy binary not found on PATH — install it via https://antigravity.google/docs/cli/install", Err.AGY_NOT_INSTALLED);
1174
+ const isAux = options.purpose === "compaction" || options.purpose === "session-title";
1175
+ if (isAux && !cfg.allowAuxiliary) throw new LlmError("auxiliary calls are disabled for the antigravity route (allowAuxiliary: false)", Err.AUX_DISABLED);
1176
+ const sessionKey = options.sessionId !== void 0 ? String(options.sessionId) : "";
1177
+ const catalog = this.deps.catalog.get();
1178
+ const model = options.model;
1179
+ const entry = findEntry(catalog, model);
1180
+ let effort;
1181
+ if (options.reasoningEffort !== void 0) {
1182
+ const wanted = String(options.reasoningEffort);
1183
+ if (entry && entry.efforts === null) throw new LlmError("model " + model + " has no selectable reasoning efforts", Err.UNSUPPORTED_REASONING_EFFORT);
1184
+ if (entry && entry.efforts && !entry.efforts.includes(wanted)) throw new LlmError("reasoning effort " + wanted + " is not supported by " + model, Err.UNSUPPORTED_REASONING_EFFORT);
1185
+ effort = wanted;
1186
+ } else if (entry && entry.efforts) effort = defaultEffortFor(entry, cfg);
1187
+ const messages = options.messages;
1188
+ let lastAssistantIdx = -1;
1189
+ for (let i = messages.length - 1; i >= 0; i--) {
1190
+ const mm = messages[i];
1191
+ if (mm !== void 0 && mm.role === "assistant") {
1192
+ lastAssistantIdx = i;
1193
+ break;
1194
+ }
1195
+ }
1196
+ const trailingUser = messages.slice(lastAssistantIdx + 1).filter((m) => m.role === "user");
1197
+ const binding = sessionKey !== "" ? this.deps.store.get(sessionKey) : void 0;
1198
+ let prompt = "";
1199
+ if (isAux && options.purpose === "compaction") {
1200
+ const cap = cfg.compactionMaxChars > 0 ? cfg.compactionMaxChars : 8e5;
1201
+ const parts = [];
1202
+ let used = 0;
1203
+ for (const m of messages) {
1204
+ const text = textOf(m);
1205
+ if (text === "") continue;
1206
+ const line = (m.role === "user" ? "User: " : "Assistant: ") + text;
1207
+ parts.push(line);
1208
+ used += line.length;
1209
+ if (used > cap) break;
1210
+ }
1211
+ prompt = "[summarize this conversation for context compaction]\n\n" + parts.join("\n\n") + "\n\nProduce a compact summary that preserves decisions, file paths, and open tasks.";
1212
+ } else {
1213
+ prompt = trailingUser.map(textOf).filter((s) => s !== "").join("\n\n");
1214
+ if (binding === void 0 && lastAssistantIdx >= 0) prompt = buildDigest(messages, 0, cfg.digestMaxChars) + prompt;
1215
+ else if (binding !== void 0) {
1216
+ const from = Math.min(binding.lastMessageCount, messages.length);
1217
+ const end = Math.max(from, lastAssistantIdx + 1);
1218
+ const span = messages.slice(from, end).filter((m) => m.role !== "assistant" || isForeignAssistant(m));
1219
+ if (span.some((m) => m.role === "assistant")) prompt = buildDigest(span, 0, cfg.digestMaxChars) + prompt;
1220
+ }
1221
+ }
1222
+ if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
1223
+ if (cfg.forwardSystemPrompt && options.system) prompt = "System instructions:\n" + options.system + "\n\n" + prompt;
1224
+ const before = snapshotConversations();
1225
+ const mapper = new EventMapper({ toolOutput: this.deps.toolOutput });
1226
+ const parser = new StreamJsonParser();
1227
+ this.deps.onParser?.(parser);
1228
+ const queue = new ChunkQueue();
1229
+ let streamCid = null;
1230
+ const args = this.buildArgs({
1231
+ prompt,
1232
+ model: model === "" ? cfg.defaultModel : model,
1233
+ effort,
1234
+ conversationId: !isAux && binding !== void 0 ? binding.conversationId : void 0,
1235
+ permissionMode: isAux ? "plan" : cfg.permissionMode,
1236
+ timeoutMs: cfg.timeoutMs,
1237
+ extraArgs: cfg.extraArgs
1238
+ });
1239
+ const release = await this.deps.acquire();
1240
+ let released = false;
1241
+ const releaseOnce = () => {
1242
+ if (released) return;
1243
+ released = true;
1244
+ release();
1245
+ };
1246
+ let proc;
1247
+ try {
1248
+ proc = startAgyProcess({
1249
+ bin,
1250
+ args,
1251
+ cwd: cfg.workspaceRoot !== "" ? cfg.workspaceRoot : void 0,
1252
+ timeoutMs: cfg.timeoutMs,
1253
+ signal: options.signal,
1254
+ onLine: (line) => {
1255
+ for (const ev of parser.feed(line + "\n")) {
1256
+ if (ev.kind === "init" && ev.conversationId) streamCid = ev.conversationId;
1257
+ if (ev.kind === "result" && ev.conversationId !== "") streamCid = ev.conversationId;
1258
+ for (const ch of mapper.map(ev)) queue.push(ch);
1259
+ }
1260
+ }
1261
+ });
1262
+ } catch (e) {
1263
+ releaseOnce();
1264
+ throw new LlmError("failed to spawn agy: " + brief(String(e)), Err.PROCESS_EXIT);
1265
+ }
1266
+ (async () => {
1267
+ const outcome = await proc.outcome;
1268
+ releaseOnce();
1269
+ for (const ev of parser.flush()) {
1270
+ if (ev.kind === "result" && ev.conversationId !== "") streamCid = ev.conversationId;
1271
+ for (const ch of mapper.map(ev)) queue.push(ch);
1272
+ }
1273
+ const diffed = diffConversations(before).conversationId;
1274
+ const conversationId = streamCid ?? diffed;
1275
+ let failure = null;
1276
+ if (outcome.aborted) failure = {
1277
+ kind: "aborted",
1278
+ code: "ABORTED",
1279
+ message: "agy run aborted by caller"
1280
+ };
1281
+ else if (outcome.timedOut) failure = {
1282
+ kind: "error",
1283
+ code: Err.TIMEOUT,
1284
+ message: "agy run exceeded the watchdog budget (" + cfg.timeoutMs + "ms)"
1285
+ };
1286
+ else if (sawAuthFailure(parser, outcome)) failure = {
1287
+ kind: "error",
1288
+ code: Err.AUTH,
1289
+ message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
1290
+ };
1291
+ else if (!mapper.isFinished) {
1292
+ if (outcome.code !== 0) failure = {
1293
+ kind: "error",
1294
+ code: Err.PROCESS_EXIT,
1295
+ message: "agy exited with code " + outcome.code + (outcome.stderrTail !== "" ? ": " + brief(outcome.stderrTail) : "")
1296
+ };
1297
+ else failure = {
1298
+ kind: "error",
1299
+ code: Err.INVALID_OUTPUT,
1300
+ message: "agy produced no result event (" + parser.stats.garbage + " unparseable lines)"
1301
+ };
1302
+ }
1303
+ if (failure !== null) for (const ch of mapper.emitFailure(failure.kind, failure.code, failure.message)) queue.push(ch);
1304
+ if (!isAux && sessionKey !== "" && failure === null) {
1305
+ const finalId = binding !== void 0 ? binding.conversationId : conversationId;
1306
+ if (finalId) this.deps.store.set(sessionKey, {
1307
+ conversationId: finalId,
1308
+ lastMessageCount: messages.length,
1309
+ updatedAt: Date.now(),
1310
+ model
1311
+ });
1312
+ }
1313
+ this.deps.onRun?.({
1314
+ ok: failure === null,
1315
+ code: failure !== null ? failure.code : "OK",
1316
+ durationMs: outcome.durationMs,
1317
+ model
1318
+ });
1319
+ queue.close();
1320
+ })().catch((err) => {
1321
+ releaseOnce();
1322
+ for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
1323
+ queue.close();
1324
+ });
1325
+ yield* queue.drain();
1326
+ }
1327
+ };
1328
+ //#endregion
1329
+ //#region src/host/oneshot.ts
1330
+ async function runAgyOnce(deps, req) {
1331
+ const cfg = deps.cfg();
1332
+ const bin = deps.bin();
1333
+ if (!bin) return {
1334
+ ok: false,
1335
+ text: "",
1336
+ conversationId: null,
1337
+ error: "agy binary not found",
1338
+ durationMs: 0
1339
+ };
1340
+ const timeoutMs = req.timeoutMs ?? cfg.timeoutMs;
1341
+ const args = [
1342
+ "--output-format",
1343
+ "stream-json",
1344
+ "--print-timeout",
1345
+ Math.max(1, Math.ceil(timeoutMs / 6e4)) + "m"
1346
+ ];
1347
+ const mode = req.mode ?? cfg.permissionMode;
1348
+ if (mode === "skip") args.push("--dangerously-skip-permissions");
1349
+ else args.push("--mode", mode);
1350
+ if (req.model) args.push("--model", req.model);
1351
+ if (req.effort) args.push("--effort", req.effort);
1352
+ args.push("-p", req.prompt);
1353
+ const parser = new StreamJsonParser();
1354
+ const textParts = [];
1355
+ let resultText = "";
1356
+ let conversationId = null;
1357
+ const outcome = await startAgyProcess({
1358
+ bin,
1359
+ args,
1360
+ cwd: cfg.workspaceRoot !== "" ? cfg.workspaceRoot : void 0,
1361
+ timeoutMs,
1362
+ signal: req.signal,
1363
+ onLine: (line) => {
1364
+ for (const ev of parser.feed(line + "\n")) {
1365
+ if (ev.kind === "init" && ev.conversationId) conversationId = ev.conversationId;
1366
+ if (ev.kind === "step" && ev.stepKind === "text" && ev.text !== "") textParts.push(ev.text);
1367
+ if (ev.kind === "result") {
1368
+ if (ev.conversationId !== "") conversationId = ev.conversationId;
1369
+ resultText = ev.response;
1370
+ }
1371
+ }
1372
+ }
1373
+ }).outcome;
1374
+ for (const ev of parser.flush()) if (ev.kind === "result") {
1375
+ if (ev.conversationId !== "") conversationId = ev.conversationId;
1376
+ resultText = ev.response;
1377
+ }
1378
+ if (outcome.aborted) return {
1379
+ ok: false,
1380
+ text: "",
1381
+ conversationId,
1382
+ error: "aborted",
1383
+ durationMs: outcome.durationMs
1384
+ };
1385
+ if (outcome.timedOut) return {
1386
+ ok: false,
1387
+ text: "",
1388
+ conversationId,
1389
+ error: "timed out after " + timeoutMs + "ms",
1390
+ durationMs: outcome.durationMs
1391
+ };
1392
+ if (looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3))) return {
1393
+ ok: false,
1394
+ text: "",
1395
+ conversationId,
1396
+ error: "agy is not signed in — run /agy auth",
1397
+ durationMs: outcome.durationMs
1398
+ };
1399
+ const joined = textParts.join("\n").trim();
1400
+ const text = joined !== "" ? joined : resultText;
1401
+ if (outcome.code !== 0 && text === "") return {
1402
+ ok: false,
1403
+ text: "",
1404
+ conversationId,
1405
+ error: "agy exited with code " + outcome.code + ": " + outcome.stderrTail.slice(-300),
1406
+ durationMs: outcome.durationMs
1407
+ };
1408
+ return {
1409
+ ok: text !== "",
1410
+ text: text === "" ? "(no output)" : text,
1411
+ conversationId,
1412
+ durationMs: outcome.durationMs
1413
+ };
1414
+ }
1415
+ //#endregion
1416
+ //#region src/host/ask-tool.ts
1417
+ const ALIASES = {
1418
+ flash: "gemini",
1419
+ pro: "gemini",
1420
+ gemini: "gemini",
1421
+ sonnet: "claude",
1422
+ opus: "claude",
1423
+ claude: "claude",
1424
+ "gpt-oss": "gpt-oss",
1425
+ oss: "gpt-oss"
1426
+ };
1427
+ /** Resolve a user-facing model word against the live catalog. */
1428
+ function resolveAskModel(input, catalog, defaultModel) {
1429
+ const q = input.trim().toLowerCase();
1430
+ if (q === "") return defaultModel;
1431
+ const ids = catalog.models.map((m) => m.id);
1432
+ if (ids.includes(q)) return q;
1433
+ const prefix = ALIASES[q] ?? q.replace(/\s+/g, "-");
1434
+ let level = "";
1435
+ for (const eff of [
1436
+ "high",
1437
+ "medium",
1438
+ "low"
1439
+ ]) if (q.endsWith(" " + eff) || q.endsWith("-" + eff)) level = eff;
1440
+ const base = level !== "" ? prefix.replace(/-?(high|medium|low)$/, "") : prefix;
1441
+ const candidates = ids.filter((id) => id.startsWith(base));
1442
+ if (candidates.length === 0) return defaultModel !== "" ? defaultModel : q;
1443
+ const sorted = candidates.slice().sort(compareNatural);
1444
+ const last = sorted[sorted.length - 1] ?? q;
1445
+ if (level === "") return last;
1446
+ return sorted.find((id) => id.endsWith("-" + level)) ?? last;
1447
+ }
1448
+ function compareNatural(a, b) {
1449
+ const na = a.split(/(\d+)/);
1450
+ const nb = b.split(/(\d+)/);
1451
+ for (let i = 0; i < Math.max(na.length, nb.length); i++) {
1452
+ const x = na[i] ?? "";
1453
+ const y = nb[i] ?? "";
1454
+ const xn = Number(x);
1455
+ const yn = Number(y);
1456
+ if (Number.isFinite(xn) && Number.isFinite(yn) && x !== "" && y !== "") {
1457
+ if (xn !== yn) return xn - yn;
1458
+ } else if (x !== y) return x < y ? -1 : 1;
1459
+ }
1460
+ return 0;
1461
+ }
1462
+ function defineAgyAskTool(deps) {
1463
+ return defineTool({
1464
+ name: "agy_ask",
1465
+ description: "Delegate a one-shot task to a Google Antigravity model via the agy CLI (e.g. ask Gemini for a review while keeping the current model). Returns the final answer text.",
1466
+ parameters: {
1467
+ prompt: {
1468
+ type: "string",
1469
+ required: true,
1470
+ description: "The task or question for the Antigravity model."
1471
+ },
1472
+ model: {
1473
+ type: "string",
1474
+ description: "Model to ask: flash, pro, gemini, sonnet, opus, gpt-oss, or an exact slug (default: the bridge default model)."
1475
+ },
1476
+ effort: {
1477
+ type: "string",
1478
+ description: "Reasoning effort for effort-capable models: low, medium, or high."
1479
+ },
1480
+ mode: {
1481
+ type: "string",
1482
+ description: "agy execution mode: plan (read-only) or accept-edits. Defaults to the bridge permission mode."
1483
+ },
1484
+ timeoutMinutes: {
1485
+ type: "number",
1486
+ description: "Optional timeout budget in minutes (default 10)."
1487
+ }
1488
+ },
1489
+ output: {
1490
+ schema: { type: "string" },
1491
+ render: (_args, value) => [{
1492
+ type: "text",
1493
+ text: value
1494
+ }]
1495
+ },
1496
+ timeoutMs: 9e5,
1497
+ async execute(args, exec) {
1498
+ exec.signal;
1499
+ const cfg = deps.cfg();
1500
+ const model = resolveAskModel(args.model ?? "", deps.catalog(), cfg.defaultModel);
1501
+ const res = await runAgyOnce(deps, {
1502
+ prompt: args.prompt,
1503
+ model: model === "" ? void 0 : model,
1504
+ effort: args.effort,
1505
+ mode: args.mode,
1506
+ timeoutMs: args.timeoutMinutes ? args.timeoutMinutes * 6e4 : void 0,
1507
+ signal: exec.signal
1508
+ });
1509
+ if (!res.ok) throw new Error("agy_ask failed: " + (res.error ?? "unknown error"));
1510
+ const footer = res.conversationId ? "\n\n(agy conversation: " + res.conversationId + " — continue it with: agy --conversation " + res.conversationId + ")" : "";
1511
+ return res.text + footer + "\n(" + Math.round(res.durationMs / 100) / 10 + "s)";
1512
+ }
1513
+ });
1514
+ }
1515
+ //#endregion
1516
+ //#region src/host/auth.ts
1517
+ const URL_WAIT_MS = 15e3;
1518
+ const CODE_SETTLE_MS = 9e4;
1519
+ var AuthHelper = class {
1520
+ bin;
1521
+ state = { phase: "idle" };
1522
+ probe = null;
1523
+ capturedUrl = null;
1524
+ urlWaiter = null;
1525
+ urlTimer = null;
1526
+ constructor(bin) {
1527
+ this.bin = bin;
1528
+ }
1529
+ status() {
1530
+ return { ...this.state };
1531
+ }
1532
+ startProbe() {
1533
+ const bin = this.bin();
1534
+ if (!bin) return null;
1535
+ this.cancel();
1536
+ this.capturedUrl = null;
1537
+ const proc = startAgyProcess({
1538
+ bin,
1539
+ args: [
1540
+ "-p",
1541
+ "ping",
1542
+ "--output-format",
1543
+ "stream-json",
1544
+ "--print-timeout",
1545
+ "4m"
1546
+ ],
1547
+ timeoutMs: 3e5,
1548
+ keepStdin: true,
1549
+ onLine: (line) => {
1550
+ if (this.capturedUrl) return;
1551
+ const url = extractAuthUrl(line);
1552
+ if (url) {
1553
+ this.capturedUrl = url;
1554
+ this.urlWaiter?.(url);
1555
+ this.urlWaiter = null;
1556
+ if (this.urlTimer) clearTimeout(this.urlTimer);
1557
+ this.urlTimer = null;
1558
+ }
1559
+ }
1560
+ });
1561
+ proc.child.stderr?.on("data", (chunk) => {
1562
+ if (this.capturedUrl) return;
1563
+ const url = extractAuthUrl(chunk);
1564
+ if (url) {
1565
+ this.capturedUrl = url;
1566
+ this.urlWaiter?.(url);
1567
+ this.urlWaiter = null;
1568
+ if (this.urlTimer) clearTimeout(this.urlTimer);
1569
+ this.urlTimer = null;
1570
+ }
1571
+ });
1572
+ this.probe = proc;
1573
+ return proc;
1574
+ }
1575
+ /** Start (or restart) the login flow; resolves with the consent URL. */
1576
+ async begin() {
1577
+ if (!this.startProbe()) {
1578
+ this.state = {
1579
+ phase: "failed",
1580
+ message: "agy binary not found"
1581
+ };
1582
+ return this.status();
1583
+ }
1584
+ const startedAt = Date.now();
1585
+ this.state = {
1586
+ phase: "pending",
1587
+ startedAt,
1588
+ expiresAt: startedAt + 55e3
1589
+ };
1590
+ const url = await new Promise((resolve) => {
1591
+ this.urlWaiter = resolve;
1592
+ this.urlTimer = setTimeout(() => {
1593
+ this.urlWaiter = null;
1594
+ resolve(null);
1595
+ }, URL_WAIT_MS);
1596
+ });
1597
+ if (this.urlTimer) clearTimeout(this.urlTimer);
1598
+ this.urlTimer = null;
1599
+ if (!url) {
1600
+ this.state = {
1601
+ phase: "ok",
1602
+ startedAt,
1603
+ message: "no login URL produced — already authenticated?"
1604
+ };
1605
+ this.cancel();
1606
+ return this.status();
1607
+ }
1608
+ this.state = {
1609
+ phase: "pending",
1610
+ url,
1611
+ startedAt,
1612
+ expiresAt: startedAt + 55e3
1613
+ };
1614
+ return this.status();
1615
+ }
1616
+ /** Pipe the pasted authorization code into the waiting probe. */
1617
+ async submitCode(code) {
1618
+ if (this.state.phase !== "pending" || !this.probe) {
1619
+ this.state = {
1620
+ phase: "failed",
1621
+ message: "no pending login — run /agy auth first"
1622
+ };
1623
+ return this.status();
1624
+ }
1625
+ if (this.state.expiresAt && Date.now() > this.state.expiresAt) {
1626
+ this.state = {
1627
+ phase: "failed",
1628
+ message: "login window expired — restart with /agy auth"
1629
+ };
1630
+ this.cancel();
1631
+ return this.status();
1632
+ }
1633
+ this.state = {
1634
+ ...this.state,
1635
+ phase: "submitting"
1636
+ };
1637
+ const proc = this.probe;
1638
+ try {
1639
+ proc.child.stdin?.write(code.trim() + "\n");
1640
+ } catch {
1641
+ this.state = {
1642
+ phase: "failed",
1643
+ message: "probe stdin closed"
1644
+ };
1645
+ return this.status();
1646
+ }
1647
+ const outcome = await Promise.race([proc.outcome, new Promise((resolve) => setTimeout(() => resolve(null), CODE_SETTLE_MS))]);
1648
+ if (outcome === null) {
1649
+ this.state = {
1650
+ phase: "failed",
1651
+ message: "timed out waiting for agy to finish the exchange"
1652
+ };
1653
+ this.cancel();
1654
+ return this.status();
1655
+ }
1656
+ const tail = outcome.stdout + outcome.stderrTail;
1657
+ if (outcome.code === 0 && !looksLikeAuthFailure(tail)) this.state = {
1658
+ phase: "ok",
1659
+ message: "authenticated"
1660
+ };
1661
+ else this.state = {
1662
+ phase: "failed",
1663
+ message: outcome.stderrTail.trim() || "authorization code rejected"
1664
+ };
1665
+ this.probe = null;
1666
+ return this.status();
1667
+ }
1668
+ cancel() {
1669
+ if (this.probe) {
1670
+ this.probe.kill("abort");
1671
+ this.probe = null;
1672
+ }
1673
+ if (this.urlTimer) clearTimeout(this.urlTimer);
1674
+ this.urlTimer = null;
1675
+ this.urlWaiter = null;
1676
+ }
1677
+ dispose() {
1678
+ this.cancel();
1679
+ this.state = { phase: "idle" };
1680
+ }
1681
+ };
1682
+ //#endregion
1683
+ //#region src/host/commands.ts
1684
+ const HELP = [
1685
+ "**/agy** — Antigravity (agy CLI) bridge",
1686
+ "- `/agy status` — binary, version, auth, mode, catalog, bindings",
1687
+ "- `/agy auth` — start Google login (returns the consent URL)",
1688
+ "- `/agy auth-code <code>` — paste the authorization code",
1689
+ "- `/agy models` — refresh and list discovered models",
1690
+ "- `/agy mode <skip|plan|accept-edits>` — permission mode (next turn)",
1691
+ "- `/agy effort <low|medium|high|default>` — default reasoning effort",
1692
+ "- `/agy clear` — drop the most recent conversation binding",
1693
+ "- `/agy doctor` — write a diagnostic report and return its path",
1694
+ "- `/agy help` — this text"
1695
+ ].join("\n");
1696
+ function agyCommandDefinition(deps) {
1697
+ return {
1698
+ name: "agy",
1699
+ description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics",
1700
+ handler: (invocation) => handle(deps, invocation.rawInput)
1701
+ };
1702
+ }
1703
+ async function handle(deps, raw) {
1704
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
1705
+ const sub = parts[0] ?? "help";
1706
+ const arg = parts[1] ?? "";
1707
+ try {
1708
+ if (sub === "status") return ok(renderStatus(deps));
1709
+ if (sub === "auth") {
1710
+ const auth = deps.auth();
1711
+ if (!auth) return err("agy binary not found — install the CLI first");
1712
+ const st = await auth.begin();
1713
+ if (st.phase === "pending" && st.url) return ok([
1714
+ "**Google login required** — open the URL, approve access, then bring the authorization code back:",
1715
+ "",
1716
+ st.url,
1717
+ "",
1718
+ "Then run: `/agy auth-code <authorization code>`",
1719
+ "The GUI panel offers the same flow with a QR code and a paste box."
1720
+ ].join("\n"));
1721
+ return ok("Auth probe: " + (st.message ?? st.phase));
1722
+ }
1723
+ if (sub === "auth-code") {
1724
+ if (arg === "") return err("usage: /agy auth-code <code>");
1725
+ const auth = deps.auth();
1726
+ if (!auth) return err("agy binary not found");
1727
+ const st = await auth.submitCode(arg);
1728
+ return st.phase === "ok" ? ok("Logged in to Antigravity.") : err(st.message ?? "login failed");
1729
+ }
1730
+ if (sub === "models") {
1731
+ const cat = await deps.catalog().forceRefresh();
1732
+ const lines = ["**Antigravity models** — source: " + cat.source + (cat.lastError === void 0 ? "" : " — " + cat.lastError) + ":"];
1733
+ for (const m of cat.models) lines.push("- `" + m.id + "` — " + m.name + (m.efforts ? " — efforts: " + m.efforts.join(" / ") : ""));
1734
+ return ok(lines.join("\n"));
1735
+ }
1736
+ if (sub === "mode") {
1737
+ if (![
1738
+ "skip",
1739
+ "plan",
1740
+ "accept-edits"
1741
+ ].includes(arg)) return err("usage: /agy mode <skip|plan|accept-edits>");
1742
+ deps.setOverride("permissionMode", arg);
1743
+ return ok("Permission mode set to **" + arg + "** — effective next turn.");
1744
+ }
1745
+ if (sub === "effort") {
1746
+ if (![
1747
+ "low",
1748
+ "medium",
1749
+ "high",
1750
+ "default"
1751
+ ].includes(arg)) return err("usage: /agy effort <low|medium|high|default>");
1752
+ deps.setOverride("defaultEffort", arg === "default" ? "" : arg);
1753
+ return ok("Default effort set to **" + (arg === "default" ? "model default" : arg) + "**.");
1754
+ }
1755
+ if (sub === "clear") {
1756
+ const all = deps.store().all();
1757
+ const keys = Object.keys(all);
1758
+ if (keys.length === 0) return ok("No conversation bindings yet.");
1759
+ const key = keys.reduce((a, b) => (all[a]?.updatedAt ?? 0) >= (all[b]?.updatedAt ?? 0) ? a : b);
1760
+ const dropped = all[key];
1761
+ if (dropped === void 0) return ok("No conversation bindings yet.");
1762
+ deps.store().delete(key);
1763
+ return ok("Dropped binding for session `" + key + "` — agy conversation " + dropped.conversationId + ". The next turn starts a fresh agy conversation.");
1764
+ }
1765
+ if (sub === "doctor") return ok("Diagnostic report written to `" + await deps.runDoctor() + "` — attach it when opening an issue.");
1766
+ return ok(HELP);
1767
+ } catch (e) {
1768
+ return err(e instanceof Error ? e.message : String(e));
1769
+ }
1770
+ }
1771
+ function renderStatus(deps) {
1772
+ const cfg = deps.cfg();
1773
+ const bin = deps.bin();
1774
+ const auth = deps.auth()?.status();
1775
+ const cat = deps.catalog().get();
1776
+ const bindings = Object.keys(deps.store().all()).length;
1777
+ const last = deps.lastRun();
1778
+ return [
1779
+ "**dsh-agy-link status**",
1780
+ "- agy binary: " + (bin ?? "not found — install via https://antigravity.google/docs/cli/install"),
1781
+ "- version: " + (deps.version() ?? "unknown"),
1782
+ "- auth: " + (auth ? auth.phase + (auth.message ? " — " + auth.message : "") : "unknown"),
1783
+ "- permission mode: " + cfg.permissionMode + (cfg.permissionMode === "skip" ? " — WARNING: agy runs tools without approval" : ""),
1784
+ "- default model: " + (cfg.defaultModel === "" ? "(agy default)" : cfg.defaultModel),
1785
+ "- default effort: " + (cfg.defaultEffort === "" ? "(model default)" : cfg.defaultEffort),
1786
+ "- catalog: " + cat.models.length + " models — " + cat.source + (cat.lastError === void 0 ? "" : " — last error: " + cat.lastError),
1787
+ "- conversation bindings: " + bindings,
1788
+ "- last run: " + (last ? (last.ok ? "ok" : last.code) + " — " + last.model + " in " + Math.round(last.durationMs / 100) / 10 + "s" : "none yet")
1789
+ ].join("\n");
1790
+ }
1791
+ function ok(text) {
1792
+ return {
1793
+ kind: "success",
1794
+ text
1795
+ };
1796
+ }
1797
+ function err(text) {
1798
+ return {
1799
+ kind: "error",
1800
+ text
1801
+ };
1802
+ }
1803
+ //#endregion
1804
+ //#region src/host/diagnostics.ts
1805
+ /** Redact auth URLs, long tokens, and authorization codes. */
1806
+ function redactLine(line) {
1807
+ let out = line;
1808
+ out = out.replace(/https:\/\/accounts\.google\.com\/\S+/g, "<auth-url-redacted>");
1809
+ out = out.replace(/\b[0-9]{4,}\//g, "4/<code-redacted>");
1810
+ out = out.replace(/ya29\.[A-Za-z0-9._-]+/g, "<oauth-token-redacted>");
1811
+ out = out.replace(/Bearer\s+\S+/gi, "Bearer <redacted>");
1812
+ return out;
1813
+ }
1814
+ function writeDoctorReport(deps) {
1815
+ const cfg = deps.cfg();
1816
+ const cat = deps.catalog().get();
1817
+ const bindings = deps.store().all();
1818
+ const lines = [];
1819
+ lines.push("# dsh-agy-link diagnostic report");
1820
+ lines.push("");
1821
+ lines.push("- generated: " + (/* @__PURE__ */ new Date()).toISOString());
1822
+ lines.push("- agy binary: " + (deps.bin() ?? "NOT FOUND"));
1823
+ lines.push("- agy version: " + (deps.version() ?? "unknown"));
1824
+ lines.push("- plugin config: " + JSON.stringify({
1825
+ ...cfg,
1826
+ extraArgs: cfg.extraArgs.length
1827
+ }));
1828
+ lines.push("- catalog: " + cat.source + " — " + cat.models.length + " models" + (cat.lastError === void 0 ? "" : " — error: " + cat.lastError));
1829
+ for (const m of cat.models) lines.push(" - " + m.id + (m.efforts ? " [" + m.efforts.join("/") + "]" : ""));
1830
+ lines.push("- conversation bindings: " + Object.keys(bindings).length);
1831
+ for (const [k, b] of Object.entries(bindings)) lines.push(" - " + k + " -> " + b.conversationId + " @ " + new Date(b.updatedAt).toISOString());
1832
+ lines.push("- node: " + process.version + " — " + process.platform + " " + process.arch);
1833
+ lines.push("");
1834
+ lines.push("## last stream-json stdout (redacted, tail)");
1835
+ lines.push("");
1836
+ lines.push("```");
1837
+ const recent = deps.recentLines();
1838
+ for (const l of recent.slice(-400)) lines.push(redactLine(l));
1839
+ lines.push("```");
1840
+ lines.push("");
1841
+ const dir = join(stateDir(), "diagnostics");
1842
+ mkdirSync(dir, { recursive: true });
1843
+ const file = join(dir, "doctor-" + (/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 14) + ".md");
1844
+ writeFileSync(file, lines.join("\n"), "utf8");
1845
+ return file;
1846
+ }
1847
+ //#endregion
1848
+ //#region src/host/diff-render.ts
1849
+ const FILE_KEYS = [
1850
+ "file_path",
1851
+ "filePath",
1852
+ "path",
1853
+ "file",
1854
+ "filename",
1855
+ "target_file",
1856
+ "absolute_path"
1857
+ ];
1858
+ function pickFile(args) {
1859
+ if (!args || typeof args !== "object") return null;
1860
+ const o = args;
1861
+ for (const k of FILE_KEYS) {
1862
+ const v = o[k];
1863
+ if (typeof v === "string" && v !== "") return v;
1864
+ }
1865
+ return null;
1866
+ }
1867
+ function looksLikeEditTool(name) {
1868
+ const n = name.toLowerCase();
1869
+ return n.includes("write") || n.includes("edit") || n.includes("replace") || n.includes("str_replace");
1870
+ }
1871
+ function renderToolActivity(name, args, output, cwd) {
1872
+ const parts = [];
1873
+ if (looksLikeEditTool(name)) {
1874
+ const file = pickFile(args);
1875
+ if (file) {
1876
+ const diff = gitDiff(file, cwd);
1877
+ if (diff !== null) parts.push("[agy edit: " + file + "]\n" + diff);
1878
+ }
1879
+ }
1880
+ if (output !== void 0 && output !== null) {
1881
+ let s;
1882
+ try {
1883
+ s = typeof output === "string" ? output : JSON.stringify(output);
1884
+ } catch {
1885
+ s = String(output);
1886
+ }
1887
+ if (s !== "") parts.push("-> " + (s.length > 2048 ? s.slice(0, 2048) + "..." : s));
1888
+ }
1889
+ return parts.length === 0 ? null : parts.join("\n") + "\n";
1890
+ }
1891
+ function gitDiff(file, cwd) {
1892
+ try {
1893
+ const out = execFileSync("git", [
1894
+ "-C",
1895
+ cwd,
1896
+ "diff",
1897
+ "HEAD",
1898
+ "--",
1899
+ file
1900
+ ], {
1901
+ encoding: "utf8",
1902
+ timeout: 5e3,
1903
+ maxBuffer: 1e6,
1904
+ stdio: [
1905
+ "ignore",
1906
+ "pipe",
1907
+ "ignore"
1908
+ ]
1909
+ });
1910
+ if (out.trim() === "") return null;
1911
+ return out.split("\n").slice(0, 100).join("\n");
1912
+ } catch {
1913
+ return null;
1914
+ }
1915
+ }
1916
+ //#endregion
1917
+ //#region src/host/sessions.ts
1918
+ var SessionStore = class {
1919
+ file;
1920
+ data = {};
1921
+ constructor(file) {
1922
+ this.file = file;
1923
+ this.load();
1924
+ }
1925
+ load() {
1926
+ try {
1927
+ if (!existsSync(this.file)) return;
1928
+ const v = JSON.parse(readFileSync(this.file, "utf8"));
1929
+ if (v && typeof v === "object") this.data = v;
1930
+ } catch {}
1931
+ }
1932
+ get(key) {
1933
+ return this.data[key];
1934
+ }
1935
+ set(key, b) {
1936
+ this.data[key] = b;
1937
+ this.persist();
1938
+ }
1939
+ delete(key) {
1940
+ delete this.data[key];
1941
+ this.persist();
1942
+ }
1943
+ all() {
1944
+ return this.data;
1945
+ }
1946
+ /** Atomic write: tmp file + rename, then merge on next load. */
1947
+ persist() {
1948
+ try {
1949
+ mkdirSync(dirname(this.file), { recursive: true });
1950
+ const tmp = join(dirname(this.file), "." + require$$basename(this.file) + ".tmp");
1951
+ writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf8");
1952
+ renameSync(tmp, this.file);
1953
+ } catch {}
1954
+ }
1955
+ };
1956
+ function require$$basename(p) {
1957
+ const i = p.lastIndexOf("/");
1958
+ return i >= 0 ? p.slice(i + 1) : p;
1959
+ }
1960
+ //#endregion
1961
+ //#region src/index.ts
1962
+ const name = "dsh-agy-link";
1963
+ const inject = ["llm", "commands"];
1964
+ /** Cross-session concurrency limiter (ADR-12). */
1965
+ var Semaphore = class {
1966
+ max;
1967
+ active = 0;
1968
+ queue = [];
1969
+ constructor(max) {
1970
+ this.max = max;
1971
+ }
1972
+ async acquire() {
1973
+ if (this.active < Math.max(1, this.max())) {
1974
+ this.active++;
1975
+ return () => this.releaseOne();
1976
+ }
1977
+ return new Promise((resolve) => {
1978
+ this.queue.push(() => {
1979
+ this.active++;
1980
+ resolve(() => this.releaseOne());
1981
+ });
1982
+ });
1983
+ }
1984
+ releaseOne() {
1985
+ this.active--;
1986
+ const next = this.queue.shift();
1987
+ if (next) next();
1988
+ }
1989
+ };
1990
+ function apply(ctx, entryConfig = {}) {
1991
+ const tag = "[dsh-agy-link] ";
1992
+ const log = (msg) => {
1993
+ ctx.logger?.info?.(tag + msg);
1994
+ };
1995
+ let binCache = void 0;
1996
+ let versionCache = null;
1997
+ let dormantReason = null;
1998
+ let lastRun = null;
1999
+ let lastParser = new StreamJsonParser();
2000
+ const getConfig = () => resolveConfig(entryConfig);
2001
+ const bin = () => {
2002
+ if (binCache === void 0) binCache = resolveAgyBin(getConfig());
2003
+ return binCache;
2004
+ };
2005
+ const version = () => versionCache;
2006
+ const store = new SessionStore(join(stateDir(), "sessions.json"));
2007
+ const semaphore = new Semaphore(() => getConfig().maxConcurrent);
2008
+ const catalog = new ModelCatalog(async (signal) => {
2009
+ const b = bin();
2010
+ if (!b) throw new Error("agy binary not found");
2011
+ const out = await probeProcess(b, [
2012
+ "models",
2013
+ "--output-format",
2014
+ "json"
2015
+ ], 3e4, signal);
2016
+ if (out.code !== 0 && out.stdout.trim() === "") throw new Error("agy models failed: " + (out.stderrTail.trim() !== "" ? out.stderrTail.trim().slice(-200) : "exit " + String(out.code)));
2017
+ return {
2018
+ stdout: out.stdout,
2019
+ stderr: out.stderrTail
2020
+ };
2021
+ }, getConfig().fallbackModels, 3e5);
2022
+ const auth = new AuthHelper(bin);
2023
+ const adapter = new AgyAdapter({
2024
+ getConfig,
2025
+ catalog,
2026
+ store,
2027
+ bin,
2028
+ acquire: () => semaphore.acquire(),
2029
+ log,
2030
+ toolOutput: (name, args, output) => {
2031
+ const ws = getConfig().workspaceRoot;
2032
+ return renderToolActivity(name, args, output, ws !== "" ? ws : process.cwd());
2033
+ },
2034
+ onRun: (info) => {
2035
+ lastRun = info;
2036
+ },
2037
+ onParser: (p) => {
2038
+ lastParser = p;
2039
+ }
2040
+ });
2041
+ const setOverride = (key, value) => {
2042
+ const file = overridesPath();
2043
+ const current = readOverrides(file);
2044
+ current[key] = value;
2045
+ try {
2046
+ mkdirSync(stateDir(), { recursive: true });
2047
+ writeFileSync(file, JSON.stringify(current, null, 2), "utf8");
2048
+ } catch (e) {
2049
+ log("failed to persist override: " + String(e));
2050
+ }
2051
+ };
2052
+ (async () => {
2053
+ if (!getConfig().enabled) {
2054
+ dormantReason = "disabled by config";
2055
+ log("dormant: disabled by config");
2056
+ return;
2057
+ }
2058
+ if (!bin()) {
2059
+ dormantReason = "agy binary not found — install via https://antigravity.google/docs/cli/install";
2060
+ log("dormant: agy binary not found");
2061
+ return;
2062
+ }
2063
+ try {
2064
+ versionCache = parseVersion((await probeProcess(bin(), ["--version"], 1e4)).stdout);
2065
+ if (versionCache && compareVersions(versionCache, "1.1.8") < 0) {
2066
+ dormantReason = "agy " + versionCache + " is older than 1.1.8 — run: agy update";
2067
+ log("dormant: " + dormantReason);
2068
+ return;
2069
+ }
2070
+ log("agy detected: " + (versionCache ?? "unknown version"));
2071
+ } catch {
2072
+ log("version probe failed — continuing with fallback catalog");
2073
+ }
2074
+ await catalog.refreshIfNeeded().catch(() => void 0);
2075
+ })();
2076
+ if (getConfig().enabled && bin()) try {
2077
+ ctx.llm.registerAdapter([PROVIDER_ID], adapter);
2078
+ log("registered provider route: antigravity");
2079
+ } catch (e) {
2080
+ log("adapter registration failed: " + String(e));
2081
+ }
2082
+ try {
2083
+ ctx.llm.registerConfigurableProviders([{
2084
+ provider: PROVIDER_ID,
2085
+ displayName: "Antigravity (agy CLI)",
2086
+ settingsNs: PLUGIN_ID,
2087
+ settingsPath: [],
2088
+ declared: false
2089
+ }]);
2090
+ } catch {}
2091
+ ctx.commands.register(agyCommandDefinition({
2092
+ cfg: getConfig,
2093
+ bin,
2094
+ version,
2095
+ auth: () => auth,
2096
+ catalog: () => catalog,
2097
+ store: () => store,
2098
+ lastRun: () => lastRun,
2099
+ setOverride,
2100
+ runDoctor: async () => {
2101
+ return writeDoctorReport({
2102
+ cfg: getConfig,
2103
+ bin,
2104
+ version,
2105
+ catalog: () => catalog,
2106
+ store: () => store,
2107
+ recentLines: () => lastParser.recentLines
2108
+ });
2109
+ }
2110
+ }));
2111
+ const toolsSvc = ctx.get("tools");
2112
+ const askToolDispose = { current: null };
2113
+ const syncAskTool = () => {
2114
+ const want = getConfig().askTool && bin() !== null;
2115
+ if (want && askToolDispose.current === null && toolsSvc) {
2116
+ const reg = toolsSvc.register(defineAgyAskTool({
2117
+ cfg: getConfig,
2118
+ bin,
2119
+ catalog: () => catalog.get()
2120
+ }));
2121
+ askToolDispose.current = typeof reg === "function" ? reg : null;
2122
+ } else if (!want && askToolDispose.current !== null) {
2123
+ askToolDispose.current();
2124
+ askToolDispose.current = null;
2125
+ }
2126
+ };
2127
+ syncAskTool();
2128
+ const webServer = ctx.get("webServer");
2129
+ const sendJson = (res, status, body) => {
2130
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
2131
+ res.end(JSON.stringify(body));
2132
+ };
2133
+ const readBody = (req) => {
2134
+ const r = req;
2135
+ return new Promise((resolve) => {
2136
+ const chunks = [];
2137
+ r.on?.("data", (c) => chunks.push(c));
2138
+ r.on?.("end", () => {
2139
+ try {
2140
+ const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
2141
+ resolve(v && typeof v === "object" ? v : {});
2142
+ } catch {
2143
+ resolve({});
2144
+ }
2145
+ });
2146
+ });
2147
+ };
2148
+ const methodOf = (req) => {
2149
+ const m = req.method;
2150
+ return typeof m === "string" ? m.toUpperCase() : "GET";
2151
+ };
2152
+ if (webServer) {
2153
+ webServer.register({
2154
+ kind: "exact",
2155
+ path: "/plugins/agy-link/status",
2156
+ handler: (_req, res) => {
2157
+ const cfg = getConfig();
2158
+ const cat = catalog.get();
2159
+ sendJson(res, 200, {
2160
+ plugin: "dsh-agy-link",
2161
+ bin: bin(),
2162
+ version: versionCache,
2163
+ dormantReason,
2164
+ enabled: cfg.enabled,
2165
+ permissionMode: cfg.permissionMode,
2166
+ defaultModel: cfg.defaultModel,
2167
+ defaultEffort: cfg.defaultEffort,
2168
+ askTool: cfg.askTool,
2169
+ auth: auth.status(),
2170
+ catalog: {
2171
+ source: cat.source,
2172
+ count: cat.models.length,
2173
+ lastError: cat.lastError ?? null
2174
+ },
2175
+ bindings: Object.keys(store.all()).length,
2176
+ lastRun
2177
+ });
2178
+ }
2179
+ });
2180
+ webServer.register({
2181
+ kind: "exact",
2182
+ path: "/plugins/agy-link/auth",
2183
+ handler: (req, res) => {
2184
+ (async () => {
2185
+ if (methodOf(req) !== "POST") {
2186
+ sendJson(res, 405, { error: "POST only" });
2187
+ return;
2188
+ }
2189
+ await readBody(req);
2190
+ const st = await auth.begin();
2191
+ sendJson(res, 200, st);
2192
+ })();
2193
+ }
2194
+ });
2195
+ webServer.register({
2196
+ kind: "exact",
2197
+ path: "/plugins/agy-link/auth-code",
2198
+ handler: (req, res) => {
2199
+ (async () => {
2200
+ if (methodOf(req) !== "POST") {
2201
+ sendJson(res, 405, { error: "POST only" });
2202
+ return;
2203
+ }
2204
+ const body = await readBody(req);
2205
+ const code = typeof body.code === "string" ? body.code : "";
2206
+ if (code === "") {
2207
+ sendJson(res, 400, { error: "missing code" });
2208
+ return;
2209
+ }
2210
+ const st = await auth.submitCode(code);
2211
+ if (st.phase === "ok") catalog.forceRefresh().catch(() => void 0);
2212
+ sendJson(res, 200, st);
2213
+ })();
2214
+ }
2215
+ });
2216
+ webServer.register({
2217
+ kind: "exact",
2218
+ path: "/plugins/agy-link/config",
2219
+ handler: (req, res) => {
2220
+ (async () => {
2221
+ if (methodOf(req) !== "POST") {
2222
+ sendJson(res, 405, { error: "POST only" });
2223
+ return;
2224
+ }
2225
+ const body = await readBody(req);
2226
+ const key = typeof body.key === "string" ? body.key : "";
2227
+ if (![
2228
+ "permissionMode",
2229
+ "defaultModel",
2230
+ "defaultEffort",
2231
+ "askTool"
2232
+ ].includes(key)) {
2233
+ sendJson(res, 400, { error: "key not settable" });
2234
+ return;
2235
+ }
2236
+ setOverride(key, body.value);
2237
+ syncAskTool();
2238
+ sendJson(res, 200, {
2239
+ ok: true,
2240
+ key,
2241
+ value: body.value
2242
+ });
2243
+ })();
2244
+ }
2245
+ });
2246
+ webServer.register({
2247
+ kind: "exact",
2248
+ path: "/plugins/agy-link/qr",
2249
+ handler: (_req, res) => {
2250
+ (async () => {
2251
+ const st = auth.status();
2252
+ const url = st.phase === "pending" ? st.url : void 0;
2253
+ if (!url) {
2254
+ const r404 = res;
2255
+ r404.writeHead(404, { "Content-Type": "text/plain" });
2256
+ r404.end("no pending auth");
2257
+ return;
2258
+ }
2259
+ try {
2260
+ const png = await (await import("qrcode")).toBuffer(url, {
2261
+ type: "png",
2262
+ width: 220,
2263
+ margin: 1
2264
+ });
2265
+ const r2 = res;
2266
+ r2.writeHead(200, {
2267
+ "Content-Type": "image/png",
2268
+ "Cache-Control": "no-store"
2269
+ });
2270
+ r2.end(png);
2271
+ } catch {
2272
+ sendJson(res, 500, { error: "qr generation failed" });
2273
+ }
2274
+ })();
2275
+ }
2276
+ });
2277
+ }
2278
+ ctx.effect(() => {
2279
+ auth.dispose();
2280
+ if (askToolDispose.current !== null) askToolDispose.current();
2281
+ return () => void 0;
2282
+ });
2283
+ }
2284
+ //#endregion
2285
+ export { apply, inject, name };