bosun 0.36.0 → 0.36.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/.env.example +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +429 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +825 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +256 -23
- package/ui/app.monolith.js +1 -1
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +101 -28
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +255 -35
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +102 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +284 -34
- package/ui/styles/components.css +47 -0
- package/ui/styles/sessions.css +75 -0
- package/ui/tabs/agents.js +73 -43
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1761 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- package/worktree-manager.mjs +1 -1
package/gemini-shell.mjs
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gemini-shell.mjs — Gemini adapter for Bosun.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* 1) Direct SDK calls via @google/genai
|
|
6
|
+
* 2) CLI fallback via the Gemini CLI binary
|
|
7
|
+
*
|
|
8
|
+
* Transport is controlled by GEMINI_TRANSPORT:
|
|
9
|
+
* auto (default) -> prefer SDK, fall back to CLI
|
|
10
|
+
* sdk -> SDK only
|
|
11
|
+
* cli -> CLI only
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import {
|
|
19
|
+
isTransientStreamError,
|
|
20
|
+
streamRetryDelay,
|
|
21
|
+
MAX_STREAM_RETRIES,
|
|
22
|
+
} from "./stream-resilience.mjs";
|
|
23
|
+
import { resolveRepoRoot } from "./repo-root.mjs";
|
|
24
|
+
|
|
25
|
+
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
26
|
+
|
|
27
|
+
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000; // 60 min for agentic task runs
|
|
28
|
+
const STATE_FILE = resolve(__dirname, "logs", "gemini-shell-state.json");
|
|
29
|
+
const REPO_ROOT = resolveRepoRoot();
|
|
30
|
+
const MAX_PROMPT_BYTES = 180_000;
|
|
31
|
+
|
|
32
|
+
let GoogleGenAIClass = null;
|
|
33
|
+
let geminiClient = null;
|
|
34
|
+
let activeTurn = false;
|
|
35
|
+
let activeSessionId = null;
|
|
36
|
+
let turnCount = 0;
|
|
37
|
+
let stateLoaded = false;
|
|
38
|
+
let activeTransport = "auto";
|
|
39
|
+
|
|
40
|
+
function timestamp() {
|
|
41
|
+
return new Date().toISOString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function envFlagEnabled(value) {
|
|
45
|
+
const raw = String(value ?? "")
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
return ["1", "true", "yes", "on", "y"].includes(raw);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveGeminiTransport() {
|
|
52
|
+
const raw = String(process.env.GEMINI_TRANSPORT || "auto")
|
|
53
|
+
.trim()
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
if (["auto", "sdk", "cli"].includes(raw)) return raw;
|
|
56
|
+
console.warn(
|
|
57
|
+
`[gemini-shell] invalid GEMINI_TRANSPORT='${raw}', defaulting to 'auto'`,
|
|
58
|
+
);
|
|
59
|
+
return "auto";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveGeminiModel(options = {}) {
|
|
63
|
+
const explicit = String(options.model || "").trim();
|
|
64
|
+
if (explicit) return explicit;
|
|
65
|
+
return String(process.env.GEMINI_MODEL || "gemini-2.5-pro").trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveGeminiApiKey() {
|
|
69
|
+
return String(
|
|
70
|
+
process.env.GEMINI_API_KEY ||
|
|
71
|
+
process.env.GOOGLE_API_KEY ||
|
|
72
|
+
"",
|
|
73
|
+
).trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveGeminiCliPath() {
|
|
77
|
+
return String(process.env.GEMINI_CLI_PATH || "gemini").trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sanitizeAndTruncatePrompt(text) {
|
|
81
|
+
if (typeof text !== "string") return "";
|
|
82
|
+
// eslint-disable-next-line no-control-regex
|
|
83
|
+
const sanitized = text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
84
|
+
const bytes = Buffer.byteLength(sanitized, "utf8");
|
|
85
|
+
if (bytes <= MAX_PROMPT_BYTES) return sanitized;
|
|
86
|
+
const buf = Buffer.from(sanitized, "utf8").slice(0, MAX_PROMPT_BYTES);
|
|
87
|
+
const truncated = buf.toString("utf8");
|
|
88
|
+
const removedBytes = bytes - MAX_PROMPT_BYTES;
|
|
89
|
+
console.warn(
|
|
90
|
+
`[gemini-shell] prompt truncated: ${bytes} → ${MAX_PROMPT_BYTES} bytes (removed ${removedBytes} bytes)`,
|
|
91
|
+
);
|
|
92
|
+
return truncated + `\n\n[...prompt truncated — ${removedBytes} bytes removed]`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function appendStatusContext(prompt, statusData) {
|
|
96
|
+
if (!statusData || typeof statusData !== "object") return prompt;
|
|
97
|
+
try {
|
|
98
|
+
const payload = JSON.stringify(statusData, null, 2);
|
|
99
|
+
return `${prompt}\n\nOrchestrator Status:\n${payload}`;
|
|
100
|
+
} catch {
|
|
101
|
+
return prompt;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function splitArgs(input) {
|
|
106
|
+
const text = String(input || "").trim();
|
|
107
|
+
if (!text) return [];
|
|
108
|
+
const out = [];
|
|
109
|
+
const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|`([^`\\]*(?:\\.[^`\\]*)*)`|([^\s]+)/g;
|
|
110
|
+
let match = re.exec(text);
|
|
111
|
+
while (match) {
|
|
112
|
+
const token =
|
|
113
|
+
match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
|
|
114
|
+
if (token) out.push(token.replace(/\\(["'`\\])/g, "$1"));
|
|
115
|
+
match = re.exec(text);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractTextFromGeminiResponse(response) {
|
|
121
|
+
if (!response) return "";
|
|
122
|
+
if (typeof response.text === "string") return response.text.trim();
|
|
123
|
+
if (typeof response.text === "function") {
|
|
124
|
+
try {
|
|
125
|
+
const text = response.text();
|
|
126
|
+
if (typeof text === "string") return text.trim();
|
|
127
|
+
} catch {
|
|
128
|
+
// no-op
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const candidates = Array.isArray(response.candidates)
|
|
132
|
+
? response.candidates
|
|
133
|
+
: [];
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
const parts = candidate?.content?.parts;
|
|
136
|
+
if (!Array.isArray(parts)) continue;
|
|
137
|
+
const merged = parts
|
|
138
|
+
.map((part) => (typeof part?.text === "string" ? part.text : ""))
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join("\n")
|
|
141
|
+
.trim();
|
|
142
|
+
if (merged) return merged;
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function extractUsage(response) {
|
|
148
|
+
const usage = response?.usageMetadata || response?.usage || null;
|
|
149
|
+
if (!usage || typeof usage !== "object") return null;
|
|
150
|
+
return {
|
|
151
|
+
promptTokens:
|
|
152
|
+
Number(usage.promptTokenCount ?? usage.input_tokens ?? usage.prompt_tokens) ||
|
|
153
|
+
null,
|
|
154
|
+
completionTokens:
|
|
155
|
+
Number(
|
|
156
|
+
usage.candidatesTokenCount ??
|
|
157
|
+
usage.output_tokens ??
|
|
158
|
+
usage.completion_tokens,
|
|
159
|
+
) || null,
|
|
160
|
+
totalTokens:
|
|
161
|
+
Number(usage.totalTokenCount ?? usage.total_tokens) || null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractTextFromCliOutput(stdout, stderr = "") {
|
|
166
|
+
const joined = String(stdout || "").trim();
|
|
167
|
+
const fallback = joined || String(stderr || "").trim();
|
|
168
|
+
if (!fallback) return "";
|
|
169
|
+
|
|
170
|
+
const tryParse = (raw) => {
|
|
171
|
+
if (!raw) return "";
|
|
172
|
+
try {
|
|
173
|
+
const data = JSON.parse(raw);
|
|
174
|
+
if (typeof data?.text === "string" && data.text.trim()) {
|
|
175
|
+
return data.text.trim();
|
|
176
|
+
}
|
|
177
|
+
if (typeof data?.output_text === "string" && data.output_text.trim()) {
|
|
178
|
+
return data.output_text.trim();
|
|
179
|
+
}
|
|
180
|
+
if (typeof data?.response?.text === "string" && data.response.text.trim()) {
|
|
181
|
+
return data.response.text.trim();
|
|
182
|
+
}
|
|
183
|
+
if (typeof data?.message === "string" && data.message.trim()) {
|
|
184
|
+
return data.message.trim();
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(data?.candidates)) {
|
|
187
|
+
for (const candidate of data.candidates) {
|
|
188
|
+
const parts = candidate?.content?.parts;
|
|
189
|
+
if (!Array.isArray(parts)) continue;
|
|
190
|
+
const text = parts
|
|
191
|
+
.map((part) => (typeof part?.text === "string" ? part.text : ""))
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join("\n")
|
|
194
|
+
.trim();
|
|
195
|
+
if (text) return text;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore parse errors
|
|
200
|
+
}
|
|
201
|
+
return "";
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const parsedWhole = tryParse(fallback);
|
|
205
|
+
if (parsedWhole) return parsedWhole;
|
|
206
|
+
for (const line of fallback.split(/\r?\n/).reverse()) {
|
|
207
|
+
const parsedLine = tryParse(line.trim());
|
|
208
|
+
if (parsedLine) return parsedLine;
|
|
209
|
+
}
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function loadGeminiSdk() {
|
|
214
|
+
if (GoogleGenAIClass) return GoogleGenAIClass;
|
|
215
|
+
try {
|
|
216
|
+
const mod = await import("@google/genai");
|
|
217
|
+
GoogleGenAIClass =
|
|
218
|
+
mod?.GoogleGenAI ||
|
|
219
|
+
mod?.default?.GoogleGenAI ||
|
|
220
|
+
mod?.default ||
|
|
221
|
+
null;
|
|
222
|
+
if (!GoogleGenAIClass) {
|
|
223
|
+
console.error("[gemini-shell] @google/genai loaded but GoogleGenAI export missing");
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
console.log("[gemini-shell] SDK loaded successfully");
|
|
227
|
+
return GoogleGenAIClass;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error(`[gemini-shell] failed to load @google/genai: ${err.message}`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function loadState() {
|
|
235
|
+
if (stateLoaded) return;
|
|
236
|
+
stateLoaded = true;
|
|
237
|
+
try {
|
|
238
|
+
const raw = await readFile(STATE_FILE, "utf8");
|
|
239
|
+
const data = JSON.parse(raw);
|
|
240
|
+
activeSessionId = data.activeSessionId || null;
|
|
241
|
+
turnCount = data.turnCount || 0;
|
|
242
|
+
activeTransport = data.activeTransport || "auto";
|
|
243
|
+
} catch {
|
|
244
|
+
activeSessionId = null;
|
|
245
|
+
turnCount = 0;
|
|
246
|
+
activeTransport = "auto";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function saveState() {
|
|
251
|
+
try {
|
|
252
|
+
await mkdir(resolve(__dirname, "logs"), { recursive: true });
|
|
253
|
+
await writeFile(
|
|
254
|
+
STATE_FILE,
|
|
255
|
+
JSON.stringify(
|
|
256
|
+
{
|
|
257
|
+
activeSessionId,
|
|
258
|
+
turnCount,
|
|
259
|
+
activeTransport,
|
|
260
|
+
updatedAt: timestamp(),
|
|
261
|
+
},
|
|
262
|
+
null,
|
|
263
|
+
2,
|
|
264
|
+
),
|
|
265
|
+
"utf8",
|
|
266
|
+
);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.warn(`[gemini-shell] failed to save state: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function ensureGeminiClient() {
|
|
273
|
+
if (geminiClient) return true;
|
|
274
|
+
const apiKey = resolveGeminiApiKey();
|
|
275
|
+
if (!apiKey) {
|
|
276
|
+
console.warn("[gemini-shell] GEMINI_API_KEY/GOOGLE_API_KEY is not set");
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const Cls = await loadGeminiSdk();
|
|
280
|
+
if (!Cls) return false;
|
|
281
|
+
try {
|
|
282
|
+
geminiClient = new Cls({ apiKey });
|
|
283
|
+
return true;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(`[gemini-shell] failed to initialize SDK client: ${err.message}`);
|
|
286
|
+
geminiClient = null;
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function withTimeout(promise, timeoutMs, abortSignal = null) {
|
|
292
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
293
|
+
let settled = false;
|
|
294
|
+
const timer = setTimeout(() => {
|
|
295
|
+
if (settled) return;
|
|
296
|
+
settled = true;
|
|
297
|
+
rejectPromise(
|
|
298
|
+
new Error(`Gemini request timed out after ${Math.round(timeoutMs / 1000)}s`),
|
|
299
|
+
);
|
|
300
|
+
}, timeoutMs);
|
|
301
|
+
|
|
302
|
+
const onAbort = () => {
|
|
303
|
+
if (settled) return;
|
|
304
|
+
settled = true;
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
rejectPromise(new Error("Gemini request aborted"));
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (abortSignal) {
|
|
310
|
+
if (abortSignal.aborted) return onAbort();
|
|
311
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Promise.resolve(promise).then(
|
|
315
|
+
(value) => {
|
|
316
|
+
if (settled) return;
|
|
317
|
+
settled = true;
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
if (abortSignal) {
|
|
320
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
321
|
+
}
|
|
322
|
+
resolvePromise(value);
|
|
323
|
+
},
|
|
324
|
+
(error) => {
|
|
325
|
+
if (settled) return;
|
|
326
|
+
settled = true;
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
if (abortSignal) {
|
|
329
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
330
|
+
}
|
|
331
|
+
rejectPromise(error);
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function runCliCommand(cliPath, args, timeoutMs, abortSignal = null) {
|
|
338
|
+
return new Promise((resolvePromise) => {
|
|
339
|
+
const child = spawn(cliPath, args, {
|
|
340
|
+
cwd: REPO_ROOT,
|
|
341
|
+
env: process.env,
|
|
342
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
343
|
+
windowsHide: true,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
let stdout = "";
|
|
347
|
+
let stderr = "";
|
|
348
|
+
let settled = false;
|
|
349
|
+
|
|
350
|
+
const timer = setTimeout(() => {
|
|
351
|
+
if (settled) return;
|
|
352
|
+
settled = true;
|
|
353
|
+
try {
|
|
354
|
+
child.kill("SIGTERM");
|
|
355
|
+
} catch {
|
|
356
|
+
// no-op
|
|
357
|
+
}
|
|
358
|
+
resolvePromise({
|
|
359
|
+
ok: false,
|
|
360
|
+
code: null,
|
|
361
|
+
stdout,
|
|
362
|
+
stderr: stderr || `Timed out after ${Math.round(timeoutMs / 1000)}s`,
|
|
363
|
+
});
|
|
364
|
+
}, timeoutMs);
|
|
365
|
+
|
|
366
|
+
const onAbort = () => {
|
|
367
|
+
if (settled) return;
|
|
368
|
+
settled = true;
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
try {
|
|
371
|
+
child.kill("SIGTERM");
|
|
372
|
+
} catch {
|
|
373
|
+
// no-op
|
|
374
|
+
}
|
|
375
|
+
resolvePromise({
|
|
376
|
+
ok: false,
|
|
377
|
+
code: null,
|
|
378
|
+
stdout,
|
|
379
|
+
stderr: "Aborted",
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (abortSignal) {
|
|
384
|
+
if (abortSignal.aborted) {
|
|
385
|
+
onAbort();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
child.stdout.on("data", (chunk) => {
|
|
392
|
+
stdout += String(chunk || "");
|
|
393
|
+
});
|
|
394
|
+
child.stderr.on("data", (chunk) => {
|
|
395
|
+
stderr += String(chunk || "");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
child.on("error", (err) => {
|
|
399
|
+
if (settled) return;
|
|
400
|
+
settled = true;
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
if (abortSignal) abortSignal.removeEventListener("abort", onAbort);
|
|
403
|
+
resolvePromise({
|
|
404
|
+
ok: false,
|
|
405
|
+
code: null,
|
|
406
|
+
stdout,
|
|
407
|
+
stderr: err.message || String(err),
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
child.on("close", (code) => {
|
|
412
|
+
if (settled) return;
|
|
413
|
+
settled = true;
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
if (abortSignal) abortSignal.removeEventListener("abort", onAbort);
|
|
416
|
+
resolvePromise({
|
|
417
|
+
ok: code === 0,
|
|
418
|
+
code,
|
|
419
|
+
stdout,
|
|
420
|
+
stderr,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function buildCliAttempts(promptText) {
|
|
427
|
+
const attempts = [];
|
|
428
|
+
const custom = String(process.env.GEMINI_CLI_ARGS || "").trim();
|
|
429
|
+
if (custom) {
|
|
430
|
+
const rawTokens = splitArgs(custom);
|
|
431
|
+
if (rawTokens.length > 0) {
|
|
432
|
+
const hasPlaceholder = rawTokens.some((token) => token.includes("{prompt}"));
|
|
433
|
+
const mapped = rawTokens.map((token) =>
|
|
434
|
+
token.replaceAll("{prompt}", promptText),
|
|
435
|
+
);
|
|
436
|
+
if (!hasPlaceholder) mapped.push(promptText);
|
|
437
|
+
attempts.push(mapped);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
attempts.push(["--prompt", promptText, "--format", "json"]);
|
|
442
|
+
attempts.push(["--prompt", promptText, "--format", "text"]);
|
|
443
|
+
attempts.push(["--prompt", promptText]);
|
|
444
|
+
attempts.push(["-p", promptText, "--format", "json"]);
|
|
445
|
+
attempts.push(["-p", promptText]);
|
|
446
|
+
attempts.push([promptText]);
|
|
447
|
+
|
|
448
|
+
return attempts;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function execGeminiCliPrompt(promptText, options = {}) {
|
|
452
|
+
const cliPath = resolveGeminiCliPath();
|
|
453
|
+
const timeoutMs = Number(options.timeoutMs) > 0
|
|
454
|
+
? Number(options.timeoutMs)
|
|
455
|
+
: DEFAULT_TIMEOUT_MS;
|
|
456
|
+
const attempts = buildCliAttempts(promptText);
|
|
457
|
+
let lastError = "Gemini CLI failed";
|
|
458
|
+
|
|
459
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
460
|
+
const args = attempts[i];
|
|
461
|
+
if (typeof options.onEvent === "function") {
|
|
462
|
+
options.onEvent(
|
|
463
|
+
`:zap: Gemini CLI (${i + 1}/${attempts.length}): ${cliPath} ${args.join(" ")}`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const result = await runCliCommand(
|
|
467
|
+
cliPath,
|
|
468
|
+
args,
|
|
469
|
+
timeoutMs,
|
|
470
|
+
options.abortController?.signal || null,
|
|
471
|
+
);
|
|
472
|
+
if (result.ok) {
|
|
473
|
+
const finalResponse = extractTextFromCliOutput(result.stdout, result.stderr);
|
|
474
|
+
return {
|
|
475
|
+
finalResponse: finalResponse || "Gemini CLI completed with no text output.",
|
|
476
|
+
items: finalResponse
|
|
477
|
+
? [{ type: "text", text: finalResponse }]
|
|
478
|
+
: [],
|
|
479
|
+
usage: null,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const stderr = String(result.stderr || "").trim();
|
|
483
|
+
if (stderr) {
|
|
484
|
+
lastError = stderr;
|
|
485
|
+
} else if (result.code !== null) {
|
|
486
|
+
lastError = `Gemini CLI exited with code ${result.code}`;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
finalResponse: `:close: Gemini CLI failed: ${lastError}`,
|
|
492
|
+
items: [],
|
|
493
|
+
usage: null,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function execGeminiPrompt(userMessage, options = {}) {
|
|
498
|
+
await loadState();
|
|
499
|
+
|
|
500
|
+
if (envFlagEnabled(process.env.GEMINI_SDK_DISABLED)) {
|
|
501
|
+
return {
|
|
502
|
+
finalResponse: ":close: Gemini adapter disabled via GEMINI_SDK_DISABLED.",
|
|
503
|
+
items: [],
|
|
504
|
+
usage: null,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (activeTurn) {
|
|
509
|
+
return {
|
|
510
|
+
finalResponse:
|
|
511
|
+
":clock: Gemini agent is still executing a previous task. Please wait.",
|
|
512
|
+
items: [],
|
|
513
|
+
usage: null,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const timeoutMs = Number(options.timeoutMs) > 0
|
|
518
|
+
? Number(options.timeoutMs)
|
|
519
|
+
: DEFAULT_TIMEOUT_MS;
|
|
520
|
+
const transport = resolveGeminiTransport();
|
|
521
|
+
activeTransport = transport;
|
|
522
|
+
|
|
523
|
+
const preferredSession = String(options.sessionId || "").trim();
|
|
524
|
+
if (preferredSession) {
|
|
525
|
+
activeSessionId = preferredSession;
|
|
526
|
+
} else if (!activeSessionId) {
|
|
527
|
+
activeSessionId = "primary-gemini";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const preparedPrompt = sanitizeAndTruncatePrompt(
|
|
531
|
+
appendStatusContext(String(userMessage || ""), options.statusData),
|
|
532
|
+
);
|
|
533
|
+
activeTurn = true;
|
|
534
|
+
let retryAttempt = 0;
|
|
535
|
+
|
|
536
|
+
while (retryAttempt <= MAX_STREAM_RETRIES) {
|
|
537
|
+
try {
|
|
538
|
+
if (transport === "cli") {
|
|
539
|
+
const cliResult = await execGeminiCliPrompt(preparedPrompt, options);
|
|
540
|
+
turnCount += 1;
|
|
541
|
+
await saveState();
|
|
542
|
+
return cliResult;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (typeof options.onEvent === "function") {
|
|
546
|
+
options.onEvent(":cpu: Gemini SDK: generating response…");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const sdkReady = await ensureGeminiClient();
|
|
550
|
+
if (!sdkReady) {
|
|
551
|
+
if (transport === "sdk") {
|
|
552
|
+
return {
|
|
553
|
+
finalResponse:
|
|
554
|
+
":close: Gemini SDK unavailable. Install @google/genai and set GEMINI_API_KEY (or GOOGLE_API_KEY), or set GEMINI_TRANSPORT=cli.",
|
|
555
|
+
items: [],
|
|
556
|
+
usage: null,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const cliResult = await execGeminiCliPrompt(preparedPrompt, options);
|
|
560
|
+
turnCount += 1;
|
|
561
|
+
await saveState();
|
|
562
|
+
return cliResult;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const model = resolveGeminiModel(options);
|
|
566
|
+
const response = await withTimeout(
|
|
567
|
+
geminiClient.models.generateContent({
|
|
568
|
+
model,
|
|
569
|
+
contents: preparedPrompt,
|
|
570
|
+
}),
|
|
571
|
+
timeoutMs,
|
|
572
|
+
options.abortController?.signal || null,
|
|
573
|
+
);
|
|
574
|
+
const finalResponse = extractTextFromGeminiResponse(response);
|
|
575
|
+
turnCount += 1;
|
|
576
|
+
await saveState();
|
|
577
|
+
return {
|
|
578
|
+
finalResponse: finalResponse || "Gemini SDK completed with no text output.",
|
|
579
|
+
items: finalResponse
|
|
580
|
+
? [{ type: "text", text: finalResponse }]
|
|
581
|
+
: [],
|
|
582
|
+
usage: extractUsage(response),
|
|
583
|
+
};
|
|
584
|
+
} catch (err) {
|
|
585
|
+
const retryable = isTransientStreamError(err) && retryAttempt < MAX_STREAM_RETRIES;
|
|
586
|
+
if (retryable) {
|
|
587
|
+
retryAttempt += 1;
|
|
588
|
+
const delay = streamRetryDelay(retryAttempt);
|
|
589
|
+
console.warn(
|
|
590
|
+
`[gemini-shell] transient error (attempt ${retryAttempt}/${MAX_STREAM_RETRIES}): ${err.message || err} — retrying in ${Math.round(delay)}ms`,
|
|
591
|
+
);
|
|
592
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, delay));
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
finalResponse: `:close: Gemini agent failed: ${err.message || String(err)}`,
|
|
597
|
+
items: [],
|
|
598
|
+
usage: null,
|
|
599
|
+
};
|
|
600
|
+
} finally {
|
|
601
|
+
activeTurn = false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
activeTurn = false;
|
|
606
|
+
return {
|
|
607
|
+
finalResponse: ":close: Gemini agent failed after all retry attempts.",
|
|
608
|
+
items: [],
|
|
609
|
+
usage: null,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export async function steerGeminiPrompt() {
|
|
614
|
+
return {
|
|
615
|
+
ok: false,
|
|
616
|
+
reason: activeTurn ? "steering_unsupported" : "idle",
|
|
617
|
+
message: activeTurn
|
|
618
|
+
? "Gemini adapter does not support steering during active turns."
|
|
619
|
+
: "No active Gemini turn.",
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function isGeminiBusy() {
|
|
624
|
+
return activeTurn;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function getSessionInfo() {
|
|
628
|
+
return {
|
|
629
|
+
sessionId: activeSessionId,
|
|
630
|
+
turnCount,
|
|
631
|
+
isActive: Boolean(activeSessionId),
|
|
632
|
+
isBusy: activeTurn,
|
|
633
|
+
transport: activeTransport,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function getActiveSessionId() {
|
|
638
|
+
return activeSessionId;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function listSessions() {
|
|
642
|
+
await loadState();
|
|
643
|
+
if (!activeSessionId) return [];
|
|
644
|
+
return [
|
|
645
|
+
{
|
|
646
|
+
id: activeSessionId,
|
|
647
|
+
title: "Gemini Session",
|
|
648
|
+
active: true,
|
|
649
|
+
turnCount,
|
|
650
|
+
},
|
|
651
|
+
];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function switchSession(id) {
|
|
655
|
+
await loadState();
|
|
656
|
+
const next = String(id || "").trim();
|
|
657
|
+
if (!next) return;
|
|
658
|
+
activeSessionId = next;
|
|
659
|
+
await saveState();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function createSession(id) {
|
|
663
|
+
await loadState();
|
|
664
|
+
const next = String(id || "").trim();
|
|
665
|
+
if (!next) {
|
|
666
|
+
throw new Error("session id required");
|
|
667
|
+
}
|
|
668
|
+
activeSessionId = next;
|
|
669
|
+
turnCount = 0;
|
|
670
|
+
await saveState();
|
|
671
|
+
return { id: next };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function resetSession() {
|
|
675
|
+
activeTurn = false;
|
|
676
|
+
activeSessionId = null;
|
|
677
|
+
turnCount = 0;
|
|
678
|
+
activeTransport = "auto";
|
|
679
|
+
geminiClient = null;
|
|
680
|
+
await saveState();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export async function initGeminiShell() {
|
|
684
|
+
await loadState();
|
|
685
|
+
if (envFlagEnabled(process.env.GEMINI_SDK_DISABLED)) return false;
|
|
686
|
+
const transport = resolveGeminiTransport();
|
|
687
|
+
activeTransport = transport;
|
|
688
|
+
if (transport === "cli") return true;
|
|
689
|
+
if (transport === "sdk") return ensureGeminiClient();
|
|
690
|
+
const sdkReady = await ensureGeminiClient();
|
|
691
|
+
return sdkReady || true;
|
|
692
|
+
}
|
package/github-oauth-portal.mjs
CHANGED
|
@@ -269,7 +269,7 @@ function htmlPage(title, bodyHtml) {
|
|
|
269
269
|
</style>
|
|
270
270
|
</head>
|
|
271
271
|
<body>
|
|
272
|
-
<div class="logo"
|
|
272
|
+
<div class="logo">:link:</div>
|
|
273
273
|
<h1>Bosun</h1>
|
|
274
274
|
<div class="subtitle">GitHub App OAuth Setup Portal · <code>bosun-ve</code></div>
|
|
275
275
|
${bodyHtml}
|
package/github-reconciler.mjs
CHANGED
|
@@ -339,7 +339,7 @@ export class GitHubReconciler {
|
|
|
339
339
|
: "";
|
|
340
340
|
await this.addComment(
|
|
341
341
|
issueNumber,
|
|
342
|
-
`##
|
|
342
|
+
`## :check: Auto-Reconciled\nThis issue was auto-closed by bosun after detecting merged PR linkage.${suffix}`,
|
|
343
343
|
);
|
|
344
344
|
}
|
|
345
345
|
summary.closed += 1;
|
|
@@ -463,7 +463,7 @@ export class GitHubReconciler {
|
|
|
463
463
|
if (this.sendTelegram) {
|
|
464
464
|
void Promise.resolve(
|
|
465
465
|
this.sendTelegram(
|
|
466
|
-
|
|
466
|
+
`:alert: GitHub reconciler cycle failed: ${msg}`,
|
|
467
467
|
),
|
|
468
468
|
).catch(() => {});
|
|
469
469
|
}
|