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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/README.zh.md +101 -0
- package/cordis.patch.yml +14 -0
- package/dist/client.js +262 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2285 -0
- package/docs/AUTH.md +19 -0
- package/docs/KNOWN-GAPS.md +41 -0
- package/docs/SECURITY-NOTES.md +29 -0
- package/docs/market-pr-draft.md +41 -0
- package/package.json +100 -0
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 };
|