@virtengine/openfleet 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/presence.mjs
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
const PRESENCE_PREFIX = "[ve-presence]";
|
|
9
|
+
const PRESENCE_VERSION = 1;
|
|
10
|
+
const INSTANCE_ID_FILENAME = "instance-id.json";
|
|
11
|
+
const PRESENCE_FILENAME = "presence.json";
|
|
12
|
+
|
|
13
|
+
const state = {
|
|
14
|
+
initialized: false,
|
|
15
|
+
repoRoot: null,
|
|
16
|
+
presencePath: null,
|
|
17
|
+
instanceId: null,
|
|
18
|
+
startedAt: new Date().toISOString(),
|
|
19
|
+
localWorkspace: null,
|
|
20
|
+
localMeta: null,
|
|
21
|
+
instances: new Map(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function safeParseNumber(value, fallback) {
|
|
25
|
+
const parsed = Number(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function ensurePresenceDir(repoRoot) {
|
|
30
|
+
const dir = resolve(repoRoot, ".cache", "openfleet");
|
|
31
|
+
await mkdir(dir, { recursive: true });
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadOrCreateInstanceId(repoRoot, explicitId) {
|
|
36
|
+
if (explicitId) {
|
|
37
|
+
return String(explicitId).trim();
|
|
38
|
+
}
|
|
39
|
+
const dir = await ensurePresenceDir(repoRoot);
|
|
40
|
+
const idPath = resolve(dir, INSTANCE_ID_FILENAME);
|
|
41
|
+
if (existsSync(idPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = await readFile(idPath, "utf8");
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (parsed?.instance_id) {
|
|
46
|
+
return String(parsed.instance_id).trim();
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// best effort
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const host = os.hostname() || "host";
|
|
53
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
54
|
+
const newId = `${host}-${suffix}`;
|
|
55
|
+
await writeFile(
|
|
56
|
+
idPath,
|
|
57
|
+
JSON.stringify({ instance_id: newId, created_at: new Date().toISOString() }, null, 2),
|
|
58
|
+
"utf8",
|
|
59
|
+
);
|
|
60
|
+
return newId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readGitInfo(repoRoot) {
|
|
64
|
+
const info = { git_branch: null, git_sha: null };
|
|
65
|
+
try {
|
|
66
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
67
|
+
cwd: repoRoot,
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
|
+
});
|
|
71
|
+
info.git_branch = branch.trim();
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const sha = execSync("git rev-parse --short HEAD", {
|
|
77
|
+
cwd: repoRoot,
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
80
|
+
});
|
|
81
|
+
info.git_sha = sha.trim();
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
return info;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildLocalMeta() {
|
|
89
|
+
const envLabel = process.env.VE_INSTANCE_LABEL || "";
|
|
90
|
+
const priority = safeParseNumber(
|
|
91
|
+
process.env.VE_COORDINATOR_PRIORITY,
|
|
92
|
+
null,
|
|
93
|
+
);
|
|
94
|
+
const eligibleRaw = process.env.VE_COORDINATOR_ELIGIBLE;
|
|
95
|
+
const eligible =
|
|
96
|
+
eligibleRaw === undefined
|
|
97
|
+
? true
|
|
98
|
+
: !["0", "false", "no"].includes(String(eligibleRaw).toLowerCase());
|
|
99
|
+
|
|
100
|
+
const workspace = state.localWorkspace || {};
|
|
101
|
+
const role = workspace.role || "workspace";
|
|
102
|
+
const basePriority = role === "coordinator" ? 10 : 100;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
v: PRESENCE_VERSION,
|
|
106
|
+
instance_id: state.instanceId,
|
|
107
|
+
instance_label: envLabel || workspace.name || null,
|
|
108
|
+
workspace_id: workspace.id || null,
|
|
109
|
+
workspace_role: role,
|
|
110
|
+
coordinator_priority: Number.isFinite(priority) ? priority : basePriority,
|
|
111
|
+
coordinator_eligible: eligible,
|
|
112
|
+
capabilities: Array.isArray(workspace.capabilities)
|
|
113
|
+
? workspace.capabilities
|
|
114
|
+
: [],
|
|
115
|
+
host: os.hostname(),
|
|
116
|
+
platform: os.platform(),
|
|
117
|
+
arch: os.arch(),
|
|
118
|
+
node: process.version,
|
|
119
|
+
pid: process.pid,
|
|
120
|
+
started_at: state.startedAt,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadPresenceRegistry() {
|
|
125
|
+
if (!state.presencePath || !existsSync(state.presencePath)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const raw = await readFile(state.presencePath, "utf8");
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (Array.isArray(parsed?.instances)) {
|
|
132
|
+
for (const entry of parsed.instances) {
|
|
133
|
+
if (entry?.instance_id) {
|
|
134
|
+
state.instances.set(String(entry.instance_id), entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function persistPresenceRegistry() {
|
|
144
|
+
if (!state.presencePath) return;
|
|
145
|
+
const instances = [...state.instances.values()];
|
|
146
|
+
const payload = {
|
|
147
|
+
updated_at: new Date().toISOString(),
|
|
148
|
+
instances,
|
|
149
|
+
};
|
|
150
|
+
await writeFile(state.presencePath, JSON.stringify(payload, null, 2), "utf8");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizePresencePayload(payload) {
|
|
154
|
+
if (!payload || !payload.instance_id) return null;
|
|
155
|
+
return {
|
|
156
|
+
...payload,
|
|
157
|
+
instance_id: String(payload.instance_id),
|
|
158
|
+
workspace_id: payload.workspace_id ? String(payload.workspace_id) : null,
|
|
159
|
+
workspace_role: payload.workspace_role || payload.role || null,
|
|
160
|
+
coordinator_priority: safeParseNumber(payload.coordinator_priority, 100),
|
|
161
|
+
coordinator_eligible:
|
|
162
|
+
payload.coordinator_eligible === undefined ? true : !!payload.coordinator_eligible,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function initPresence(options = {}) {
|
|
167
|
+
const forceReset = options.force || process.env.VITEST;
|
|
168
|
+
if (state.initialized && !forceReset) return state;
|
|
169
|
+
if (forceReset) {
|
|
170
|
+
state.initialized = false;
|
|
171
|
+
state.repoRoot = null;
|
|
172
|
+
state.presencePath = null;
|
|
173
|
+
state.instanceId = null;
|
|
174
|
+
state.startedAt = new Date().toISOString();
|
|
175
|
+
state.localWorkspace = null;
|
|
176
|
+
state.localMeta = null;
|
|
177
|
+
state.instances = new Map();
|
|
178
|
+
}
|
|
179
|
+
state.repoRoot = options.repoRoot || process.cwd();
|
|
180
|
+
state.presencePath =
|
|
181
|
+
options.presencePath ||
|
|
182
|
+
resolve(state.repoRoot, ".cache", "openfleet", PRESENCE_FILENAME);
|
|
183
|
+
state.localWorkspace = options.localWorkspace || null;
|
|
184
|
+
state.instanceId = await loadOrCreateInstanceId(
|
|
185
|
+
state.repoRoot,
|
|
186
|
+
options.instanceId || process.env.VE_INSTANCE_ID,
|
|
187
|
+
);
|
|
188
|
+
state.localMeta = buildLocalMeta();
|
|
189
|
+
await ensurePresenceDir(state.repoRoot);
|
|
190
|
+
const shouldLoadRegistry =
|
|
191
|
+
options.loadRegistry ?? (!options.skipLoad && !process.env.VITEST);
|
|
192
|
+
if (shouldLoadRegistry) {
|
|
193
|
+
await loadPresenceRegistry();
|
|
194
|
+
}
|
|
195
|
+
state.initialized = true;
|
|
196
|
+
return state;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getPresencePrefix() {
|
|
200
|
+
return PRESENCE_PREFIX;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatPresenceMessage(payload) {
|
|
204
|
+
return `${PRESENCE_PREFIX} ${JSON.stringify(payload)}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function parsePresenceMessage(text) {
|
|
208
|
+
const raw = String(text || "");
|
|
209
|
+
const idx = raw.indexOf(PRESENCE_PREFIX);
|
|
210
|
+
if (idx === -1) return null;
|
|
211
|
+
const jsonPart = raw.slice(idx + PRESENCE_PREFIX.length).trim();
|
|
212
|
+
if (!jsonPart.startsWith("{")) return null;
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(jsonPart);
|
|
215
|
+
return normalizePresencePayload(parsed);
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function buildLocalPresence(extra = {}) {
|
|
222
|
+
const meta = state.localMeta || {};
|
|
223
|
+
const gitInfo = state.repoRoot ? readGitInfo(state.repoRoot) : {};
|
|
224
|
+
return normalizePresencePayload({
|
|
225
|
+
...meta,
|
|
226
|
+
...gitInfo,
|
|
227
|
+
...extra,
|
|
228
|
+
updated_at: new Date().toISOString(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function notePresence(payload, options = {}) {
|
|
233
|
+
const normalized = normalizePresencePayload(payload);
|
|
234
|
+
if (!normalized) return null;
|
|
235
|
+
const now = options.receivedAt || new Date().toISOString();
|
|
236
|
+
const entry = {
|
|
237
|
+
...normalized,
|
|
238
|
+
last_seen_at: now,
|
|
239
|
+
source: options.source || normalized.source || "telegram",
|
|
240
|
+
};
|
|
241
|
+
state.instances.set(normalized.instance_id, entry);
|
|
242
|
+
await persistPresenceRegistry();
|
|
243
|
+
return entry;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function listActiveInstances({ nowMs, ttlMs } = {}) {
|
|
247
|
+
const now = Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
248
|
+
const ttl = Number.isFinite(ttlMs) ? ttlMs : 0;
|
|
249
|
+
const instances = [];
|
|
250
|
+
for (const entry of state.instances.values()) {
|
|
251
|
+
const last = Date.parse(entry.last_seen_at || entry.updated_at || "");
|
|
252
|
+
if (ttl > 0 && (!Number.isFinite(last) || now - last >= ttl)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
instances.push(entry);
|
|
256
|
+
}
|
|
257
|
+
return instances;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function selectCoordinator({ nowMs, ttlMs } = {}) {
|
|
261
|
+
const active = listActiveInstances({ nowMs, ttlMs });
|
|
262
|
+
if (!active.length) return null;
|
|
263
|
+
const eligible = active.filter((entry) => entry.coordinator_eligible !== false);
|
|
264
|
+
const pool = eligible.length ? eligible : active;
|
|
265
|
+
const preferred = pool.filter(
|
|
266
|
+
(entry) => String(entry.workspace_role || "").toLowerCase() === "coordinator",
|
|
267
|
+
);
|
|
268
|
+
const candidates = preferred.length ? preferred : pool;
|
|
269
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
270
|
+
const pa = safeParseNumber(a.coordinator_priority, 100);
|
|
271
|
+
const pb = safeParseNumber(b.coordinator_priority, 100);
|
|
272
|
+
if (pa !== pb) return pa - pb;
|
|
273
|
+
const sa = Date.parse(a.started_at || "");
|
|
274
|
+
const sb = Date.parse(b.started_at || "");
|
|
275
|
+
if (Number.isFinite(sa) && Number.isFinite(sb) && sa !== sb) {
|
|
276
|
+
return sa - sb;
|
|
277
|
+
}
|
|
278
|
+
return String(a.instance_id).localeCompare(String(b.instance_id));
|
|
279
|
+
});
|
|
280
|
+
return sorted[0] || null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function formatPresenceSummary({ nowMs, ttlMs } = {}) {
|
|
284
|
+
const active = listActiveInstances({ nowMs, ttlMs });
|
|
285
|
+
if (!active.length) {
|
|
286
|
+
return "No active instances reported.";
|
|
287
|
+
}
|
|
288
|
+
const coordinator = selectCoordinator({ nowMs, ttlMs });
|
|
289
|
+
const lines = ["🛰️ Codex Monitor Presence"];
|
|
290
|
+
for (const entry of active) {
|
|
291
|
+
const name = entry.instance_label || entry.instance_id;
|
|
292
|
+
const role = entry.workspace_role || "workspace";
|
|
293
|
+
const host = entry.host || "unknown";
|
|
294
|
+
const lastSeen = entry.last_seen_at
|
|
295
|
+
? entry.last_seen_at.slice(11, 19)
|
|
296
|
+
: "--:--:--";
|
|
297
|
+
const coordTag =
|
|
298
|
+
coordinator && coordinator.instance_id === entry.instance_id ? " (coordinator)" : "";
|
|
299
|
+
lines.push(`- ${name}${coordTag} — ${role} @ ${host} (last ${lastSeen})`);
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function formatCoordinatorSummary({ nowMs, ttlMs } = {}) {
|
|
305
|
+
const coordinator = selectCoordinator({ nowMs, ttlMs });
|
|
306
|
+
if (!coordinator) {
|
|
307
|
+
return "No coordinator selected (no active instances).";
|
|
308
|
+
}
|
|
309
|
+
const name = coordinator.instance_label || coordinator.instance_id;
|
|
310
|
+
const role = coordinator.workspace_role || "workspace";
|
|
311
|
+
const host = coordinator.host || "unknown";
|
|
312
|
+
const lastSeen = coordinator.last_seen_at || coordinator.updated_at || "unknown";
|
|
313
|
+
return [
|
|
314
|
+
"⭐ Coordinator",
|
|
315
|
+
`Instance: ${name}`,
|
|
316
|
+
`Role: ${role}`,
|
|
317
|
+
`Host: ${host}`,
|
|
318
|
+
`Last seen: ${lastSeen}`,
|
|
319
|
+
].join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function getPresenceState() {
|
|
323
|
+
return {
|
|
324
|
+
instance_id: state.instanceId,
|
|
325
|
+
started_at: state.startedAt,
|
|
326
|
+
instances: [...state.instances.values()],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* primary-agent.mjs — Adapter that selects the primary agent implementation.
|
|
3
|
+
*
|
|
4
|
+
* Supports Codex SDK, Copilot SDK, and Claude SDK.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadConfig } from "./config.mjs";
|
|
8
|
+
import { getSessionTracker } from "./session-tracker.mjs";
|
|
9
|
+
import {
|
|
10
|
+
execCodexPrompt,
|
|
11
|
+
steerCodexPrompt,
|
|
12
|
+
isCodexBusy,
|
|
13
|
+
getThreadInfo,
|
|
14
|
+
resetThread,
|
|
15
|
+
initCodexShell,
|
|
16
|
+
getActiveSessionId as getCodexSessionId,
|
|
17
|
+
listSessions as listCodexSessions,
|
|
18
|
+
switchSession as switchCodexSession,
|
|
19
|
+
createSession as createCodexSession,
|
|
20
|
+
} from "./codex-shell.mjs";
|
|
21
|
+
import {
|
|
22
|
+
execCopilotPrompt,
|
|
23
|
+
steerCopilotPrompt,
|
|
24
|
+
isCopilotBusy,
|
|
25
|
+
getSessionInfo as getCopilotSessionInfo,
|
|
26
|
+
resetSession as resetCopilotSession,
|
|
27
|
+
initCopilotShell,
|
|
28
|
+
} from "./copilot-shell.mjs";
|
|
29
|
+
import {
|
|
30
|
+
execClaudePrompt,
|
|
31
|
+
steerClaudePrompt,
|
|
32
|
+
isClaudeBusy,
|
|
33
|
+
getSessionInfo as getClaudeSessionInfo,
|
|
34
|
+
resetClaudeSession,
|
|
35
|
+
initClaudeShell,
|
|
36
|
+
} from "./claude-shell.mjs";
|
|
37
|
+
|
|
38
|
+
const ADAPTERS = {
|
|
39
|
+
"codex-sdk": {
|
|
40
|
+
name: "codex-sdk",
|
|
41
|
+
provider: "CODEX",
|
|
42
|
+
exec: (msg, opts) => execCodexPrompt(msg, { persistent: true, ...opts }),
|
|
43
|
+
steer: steerCodexPrompt,
|
|
44
|
+
isBusy: isCodexBusy,
|
|
45
|
+
getInfo: () => {
|
|
46
|
+
const info = getThreadInfo();
|
|
47
|
+
return { ...info, sessionId: info.sessionId || info.threadId };
|
|
48
|
+
},
|
|
49
|
+
reset: resetThread,
|
|
50
|
+
init: async () => {
|
|
51
|
+
await initCodexShell();
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
getSessionId: getCodexSessionId,
|
|
55
|
+
listSessions: listCodexSessions,
|
|
56
|
+
switchSession: switchCodexSession,
|
|
57
|
+
createSession: createCodexSession,
|
|
58
|
+
},
|
|
59
|
+
"copilot-sdk": {
|
|
60
|
+
name: "copilot-sdk",
|
|
61
|
+
provider: "COPILOT",
|
|
62
|
+
exec: (msg, opts) => execCopilotPrompt(msg, { persistent: true, ...opts }),
|
|
63
|
+
steer: steerCopilotPrompt,
|
|
64
|
+
isBusy: isCopilotBusy,
|
|
65
|
+
getInfo: () => getCopilotSessionInfo(),
|
|
66
|
+
reset: resetCopilotSession,
|
|
67
|
+
init: async () => initCopilotShell(),
|
|
68
|
+
},
|
|
69
|
+
"claude-sdk": {
|
|
70
|
+
name: "claude-sdk",
|
|
71
|
+
provider: "CLAUDE",
|
|
72
|
+
exec: execClaudePrompt,
|
|
73
|
+
steer: steerClaudePrompt,
|
|
74
|
+
isBusy: isClaudeBusy,
|
|
75
|
+
getInfo: () => getClaudeSessionInfo(),
|
|
76
|
+
reset: resetClaudeSession,
|
|
77
|
+
init: async () => {
|
|
78
|
+
await initClaudeShell();
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function envFlagEnabled(value) {
|
|
85
|
+
const raw = String(value ?? "")
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase();
|
|
88
|
+
return ["1", "true", "yes", "on", "y"].includes(raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let activeAdapter = ADAPTERS["codex-sdk"];
|
|
92
|
+
let primaryProfile = null;
|
|
93
|
+
let primaryFallbackReason = null;
|
|
94
|
+
let initialized = false;
|
|
95
|
+
|
|
96
|
+
function normalizePrimaryAgent(value) {
|
|
97
|
+
const raw = String(value || "")
|
|
98
|
+
.trim()
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
if (!raw) return "codex-sdk";
|
|
101
|
+
if (["codex", "codex-sdk"].includes(raw)) return "codex-sdk";
|
|
102
|
+
if (["copilot", "copilot-sdk", "github-copilot"].includes(raw))
|
|
103
|
+
return "copilot-sdk";
|
|
104
|
+
if (["claude", "claude-sdk", "claude_code", "claude-code"].includes(raw))
|
|
105
|
+
return "claude-sdk";
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function selectPrimaryExecutor(config) {
|
|
110
|
+
const executors = config?.executorConfig?.executors || [];
|
|
111
|
+
if (!executors.length) return null;
|
|
112
|
+
const primary = executors.find(
|
|
113
|
+
(e) => (e.role || "").toLowerCase() === "primary",
|
|
114
|
+
);
|
|
115
|
+
return primary || executors[0];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function executorToAdapter(executor) {
|
|
119
|
+
if (!executor) return null;
|
|
120
|
+
const normalized = String(executor).toUpperCase();
|
|
121
|
+
if (normalized === "COPILOT") return "copilot-sdk";
|
|
122
|
+
if (normalized === "CLAUDE") return "claude-sdk";
|
|
123
|
+
return "codex-sdk";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolvePrimaryAgent(nameOrConfig) {
|
|
127
|
+
if (typeof nameOrConfig === "string" && nameOrConfig.trim()) {
|
|
128
|
+
return normalizePrimaryAgent(nameOrConfig);
|
|
129
|
+
}
|
|
130
|
+
if (nameOrConfig && typeof nameOrConfig === "object") {
|
|
131
|
+
const direct = normalizePrimaryAgent(nameOrConfig.primaryAgent);
|
|
132
|
+
if (direct) return direct;
|
|
133
|
+
}
|
|
134
|
+
if (process.env.PRIMARY_AGENT || process.env.PRIMARY_AGENT_SDK) {
|
|
135
|
+
return normalizePrimaryAgent(
|
|
136
|
+
process.env.PRIMARY_AGENT || process.env.PRIMARY_AGENT_SDK,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const cfg = loadConfig();
|
|
140
|
+
const direct = normalizePrimaryAgent(cfg?.primaryAgent || "");
|
|
141
|
+
if (direct) return direct;
|
|
142
|
+
primaryProfile = selectPrimaryExecutor(cfg);
|
|
143
|
+
const mapped = executorToAdapter(primaryProfile?.executor);
|
|
144
|
+
return mapped || "codex-sdk";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function setPrimaryAgent(name) {
|
|
148
|
+
const normalized = normalizePrimaryAgent(name);
|
|
149
|
+
activeAdapter = ADAPTERS[normalized] || ADAPTERS["codex-sdk"];
|
|
150
|
+
return activeAdapter.name;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getPrimaryAgentName() {
|
|
154
|
+
return activeAdapter?.name || "codex-sdk";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function switchPrimaryAgent(name) {
|
|
158
|
+
const normalized = normalizePrimaryAgent(name);
|
|
159
|
+
if (!ADAPTERS[normalized]) {
|
|
160
|
+
return { ok: false, reason: "unknown_agent" };
|
|
161
|
+
}
|
|
162
|
+
activeAdapter = ADAPTERS[normalized];
|
|
163
|
+
primaryFallbackReason = null;
|
|
164
|
+
initialized = false;
|
|
165
|
+
try {
|
|
166
|
+
await initPrimaryAgent(normalized);
|
|
167
|
+
return { ok: true, name: getPrimaryAgentName() };
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return { ok: false, reason: err?.message || "init_failed" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function initPrimaryAgent(nameOrConfig = null) {
|
|
174
|
+
if (initialized) return getPrimaryAgentName();
|
|
175
|
+
const desired = resolvePrimaryAgent(nameOrConfig);
|
|
176
|
+
setPrimaryAgent(desired);
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
activeAdapter.name === "codex-sdk" &&
|
|
180
|
+
envFlagEnabled(process.env.CODEX_SDK_DISABLED)
|
|
181
|
+
) {
|
|
182
|
+
primaryFallbackReason = "Codex SDK disabled — attempting fallback";
|
|
183
|
+
if (!envFlagEnabled(process.env.COPILOT_SDK_DISABLED)) {
|
|
184
|
+
setPrimaryAgent("copilot-sdk");
|
|
185
|
+
} else if (!envFlagEnabled(process.env.CLAUDE_SDK_DISABLED)) {
|
|
186
|
+
setPrimaryAgent("claude-sdk");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
activeAdapter.name === "claude-sdk" &&
|
|
192
|
+
envFlagEnabled(process.env.CLAUDE_SDK_DISABLED)
|
|
193
|
+
) {
|
|
194
|
+
primaryFallbackReason = "Claude SDK disabled — falling back to Codex";
|
|
195
|
+
setPrimaryAgent("codex-sdk");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ok = await activeAdapter.init();
|
|
199
|
+
if (activeAdapter.name === "copilot-sdk" && ok === false) {
|
|
200
|
+
primaryFallbackReason = "Copilot SDK unavailable — falling back to Codex";
|
|
201
|
+
setPrimaryAgent("codex-sdk");
|
|
202
|
+
await activeAdapter.init();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
initialized = true;
|
|
206
|
+
return getPrimaryAgentName();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
210
|
+
if (!initialized) {
|
|
211
|
+
await initPrimaryAgent();
|
|
212
|
+
}
|
|
213
|
+
const sessionId = `primary-${activeAdapter.name}`;
|
|
214
|
+
const tracker = getSessionTracker();
|
|
215
|
+
tracker.recordEvent(sessionId, {
|
|
216
|
+
role: "user",
|
|
217
|
+
content: userMessage,
|
|
218
|
+
timestamp: new Date().toISOString(),
|
|
219
|
+
_sessionType: "primary",
|
|
220
|
+
});
|
|
221
|
+
const result = await activeAdapter.exec(userMessage, options);
|
|
222
|
+
if (result) {
|
|
223
|
+
tracker.recordEvent(sessionId, {
|
|
224
|
+
role: "assistant",
|
|
225
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
_sessionType: "primary",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function steerPrimaryPrompt(message) {
|
|
234
|
+
if (!initialized) {
|
|
235
|
+
await initPrimaryAgent();
|
|
236
|
+
}
|
|
237
|
+
return activeAdapter.steer(message);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function isPrimaryBusy() {
|
|
241
|
+
return activeAdapter.isBusy();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function getPrimaryAgentInfo() {
|
|
245
|
+
const info = activeAdapter.getInfo ? activeAdapter.getInfo() : {};
|
|
246
|
+
return {
|
|
247
|
+
adapter: activeAdapter.name,
|
|
248
|
+
provider: activeAdapter.provider,
|
|
249
|
+
profile: primaryProfile,
|
|
250
|
+
fallbackReason: primaryFallbackReason,
|
|
251
|
+
sessionId: info.sessionId || info.threadId || null,
|
|
252
|
+
threadId: info.threadId || null,
|
|
253
|
+
turnCount: info.turnCount || 0,
|
|
254
|
+
isActive: !!info.isActive,
|
|
255
|
+
isBusy: !!info.isBusy,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function resetPrimaryAgent() {
|
|
260
|
+
if (!initialized) {
|
|
261
|
+
await initPrimaryAgent();
|
|
262
|
+
}
|
|
263
|
+
if (activeAdapter.reset) {
|
|
264
|
+
await activeAdapter.reset();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function getPrimarySessionId() {
|
|
269
|
+
return activeAdapter.getSessionId ? activeAdapter.getSessionId() : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function listPrimarySessions() {
|
|
273
|
+
return activeAdapter.listSessions ? activeAdapter.listSessions() : [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function switchPrimarySession(id) {
|
|
277
|
+
return activeAdapter.switchSession ? activeAdapter.switchSession(id) : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function createPrimarySession(id) {
|
|
281
|
+
return activeAdapter.createSession ? activeAdapter.createSession(id) : undefined;
|
|
282
|
+
}
|