@triflux/remote 10.31.0 → 10.33.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/cto/brief.mjs +93 -0
- package/cto/collect.mjs +846 -0
- package/cto/current.schema.json +181 -0
- package/cto/status.mjs +260 -0
- package/hub/hub-lifecycle.mjs +242 -0
- package/hub/mac-focus.mjs +94 -0
- package/hub/mac-tray.swift +133 -0
- package/hub/promote-penalties.mjs +138 -0
- package/hub/public/tray.html +594 -0
- package/hub/server.mjs +233 -44
- package/hub/team/claude-daemon-control.mjs +46 -46
- package/hub/team/conductor.mjs +18 -0
- package/hub/team/cto-auto-collect.mjs +109 -0
- package/hub/team/cto-pull.mjs +27 -0
- package/hub/team/uds-orchestrator.mjs +607 -0
- package/hub/tray-lifecycle.mjs +131 -0
- package/hub/tray-runtime.mjs +265 -0
- package/hub/tray-state.mjs +448 -0
- package/hub/tray.mjs +93 -4
- package/hub/workers/antigravity-route-worker.mjs +14 -4
- package/hub/workers/delegator-mcp.mjs +2 -5
- package/hub/workers/factory.mjs +1 -1
- package/hub/workers/gemini-worker.mjs +2 -1
- package/package.json +7 -2
- package/scripts/lib/env-probe.mjs +36 -9
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// hub/tray-state.mjs — normalize Hub/CTO/MCP state for the macOS tray UI
|
|
2
|
+
|
|
3
|
+
function toIsoTime(value) {
|
|
4
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6
|
+
return new Date(value).toISOString();
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function statusRank(status) {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case "active":
|
|
14
|
+
return 0;
|
|
15
|
+
case "idle":
|
|
16
|
+
return 1;
|
|
17
|
+
case "starting":
|
|
18
|
+
return 2;
|
|
19
|
+
case "stale":
|
|
20
|
+
return 4;
|
|
21
|
+
default:
|
|
22
|
+
return 3;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readSynapseSessions(snapshot) {
|
|
27
|
+
if (Array.isArray(snapshot)) return snapshot;
|
|
28
|
+
if (Array.isArray(snapshot?.sessions)) return snapshot.sessions;
|
|
29
|
+
if (snapshot && typeof snapshot === "object") {
|
|
30
|
+
return Object.values(snapshot).filter(
|
|
31
|
+
(entry) => entry && typeof entry === "object" && entry.sessionId,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MCP_CLIENTS = Object.freeze([
|
|
38
|
+
{ id: "claude", label: "Claude", icon: "C", color: "#d19a66" },
|
|
39
|
+
{ id: "codex", label: "Codex", icon: "X", color: "#ffffff" },
|
|
40
|
+
{ id: "antigravity", label: "Antigravity", icon: "A", color: "#61afef" },
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function normalizeAgentId(value) {
|
|
44
|
+
const id = String(value || "")
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
if (id === "agy" || id === "anti") return "antigravity";
|
|
48
|
+
if (id === "claude" || id === "codex" || id === "antigravity") return id;
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inferAgentId(value = {}) {
|
|
53
|
+
const direct = normalizeAgentId(
|
|
54
|
+
value.agent_id ??
|
|
55
|
+
value.agentId ??
|
|
56
|
+
value.client ??
|
|
57
|
+
value.provider ??
|
|
58
|
+
value.runtimeClient,
|
|
59
|
+
);
|
|
60
|
+
if (direct) return direct;
|
|
61
|
+
const haystack = [
|
|
62
|
+
value.sessionId,
|
|
63
|
+
value.sessionKind,
|
|
64
|
+
value.role,
|
|
65
|
+
value.taskSummary,
|
|
66
|
+
value.command,
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(" ")
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
for (const id of ["claude", "codex", "antigravity"]) {
|
|
72
|
+
if (haystack.includes(id)) return id;
|
|
73
|
+
}
|
|
74
|
+
if (haystack.includes("agy")) return "antigravity";
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function agentMeta(agentId) {
|
|
79
|
+
const id = normalizeAgentId(agentId);
|
|
80
|
+
const meta = MCP_CLIENTS.find((client) => client.id === id);
|
|
81
|
+
return meta ? { ...meta } : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function normalizeTraySessions(snapshot) {
|
|
85
|
+
return readSynapseSessions(snapshot)
|
|
86
|
+
.map((session) => {
|
|
87
|
+
const agentId = inferAgentId(session);
|
|
88
|
+
return {
|
|
89
|
+
sessionId: String(session?.sessionId || ""),
|
|
90
|
+
host: typeof session?.host === "string" ? session.host : "local",
|
|
91
|
+
status:
|
|
92
|
+
typeof session?.status === "string"
|
|
93
|
+
? session.status
|
|
94
|
+
: typeof session?.phase === "string"
|
|
95
|
+
? session.phase
|
|
96
|
+
: "active",
|
|
97
|
+
sessionKind:
|
|
98
|
+
typeof session?.sessionKind === "string"
|
|
99
|
+
? session.sessionKind
|
|
100
|
+
: "interactive",
|
|
101
|
+
cwd: typeof session?.cwd === "string" ? session.cwd : "",
|
|
102
|
+
worktreePath:
|
|
103
|
+
typeof session?.worktreePath === "string" ? session.worktreePath : "",
|
|
104
|
+
branch: typeof session?.branch === "string" ? session.branch : "",
|
|
105
|
+
taskSummary:
|
|
106
|
+
typeof session?.taskSummary === "string" ? session.taskSummary : "",
|
|
107
|
+
agent_id: agentId || null,
|
|
108
|
+
agent: agentMeta(agentId),
|
|
109
|
+
lastHeartbeat: Number.isFinite(Number(session?.lastHeartbeat))
|
|
110
|
+
? Number(session.lastHeartbeat)
|
|
111
|
+
: null,
|
|
112
|
+
started_at: toIsoTime(
|
|
113
|
+
session?.started_at ?? session?.startedAt ?? session?.lastHeartbeat,
|
|
114
|
+
),
|
|
115
|
+
isRemote: session?.isRemote === true,
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
.filter((session) => session.sessionId)
|
|
119
|
+
.sort((a, b) => {
|
|
120
|
+
const rankDelta = statusRank(a.status) - statusRank(b.status);
|
|
121
|
+
if (rankDelta !== 0) return rankDelta;
|
|
122
|
+
return (b.lastHeartbeat || 0) - (a.lastHeartbeat || 0);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function countBy(items, pickKey) {
|
|
127
|
+
const counts = {};
|
|
128
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
129
|
+
const key = pickKey(item) || "unknown";
|
|
130
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
return counts;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sanitizeMcpRow(row) {
|
|
136
|
+
return {
|
|
137
|
+
type: String(row?.type || ""),
|
|
138
|
+
name: String(row?.name || ""),
|
|
139
|
+
client: String(row?.client || ""),
|
|
140
|
+
label: String(row?.label || ""),
|
|
141
|
+
filePath: String(row?.filePath || ""),
|
|
142
|
+
expectedUrl: String(row?.expectedUrl || ""),
|
|
143
|
+
expectedCommand: String(row?.expectedCommand || ""),
|
|
144
|
+
actualUrl: String(row?.actualUrl || ""),
|
|
145
|
+
actualCommand: String(row?.actualCommand || ""),
|
|
146
|
+
status: String(row?.status || ""),
|
|
147
|
+
headerStatus: String(row?.headerStatus || ""),
|
|
148
|
+
expectedHeaderNames: Array.isArray(row?.expectedHeaderNames)
|
|
149
|
+
? row.expectedHeaderNames.map(String)
|
|
150
|
+
: [],
|
|
151
|
+
actualHeaderNames: Array.isArray(row?.actualHeaderNames)
|
|
152
|
+
? row.actualHeaderNames.map(String)
|
|
153
|
+
: [],
|
|
154
|
+
command: row?.command ? String(row.command) : "",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function summarizeMcpClients(rows) {
|
|
159
|
+
return MCP_CLIENTS.map((client) => {
|
|
160
|
+
const clientRows = rows.filter(
|
|
161
|
+
(row) => row.type === "registry" && row.client === client.id,
|
|
162
|
+
);
|
|
163
|
+
const present = clientRows.filter(
|
|
164
|
+
(row) => row.status === "present" || row.status === "ok",
|
|
165
|
+
).length;
|
|
166
|
+
const total = clientRows.length;
|
|
167
|
+
let status = "off";
|
|
168
|
+
if (total > 0 && present === total) status = "on";
|
|
169
|
+
else if (present > 0) status = "partial";
|
|
170
|
+
return {
|
|
171
|
+
...client,
|
|
172
|
+
status,
|
|
173
|
+
present,
|
|
174
|
+
total,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function summarizeMcpServers(rows) {
|
|
180
|
+
const grouped = new Map();
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
if (row.type !== "registry" || !row.name) continue;
|
|
183
|
+
if (!grouped.has(row.name)) {
|
|
184
|
+
grouped.set(row.name, {
|
|
185
|
+
name: row.name,
|
|
186
|
+
target:
|
|
187
|
+
row.expectedUrl ||
|
|
188
|
+
row.actualUrl ||
|
|
189
|
+
row.expectedCommand ||
|
|
190
|
+
row.actualCommand ||
|
|
191
|
+
"",
|
|
192
|
+
rows: [],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const group = grouped.get(row.name);
|
|
196
|
+
if (!group.target) {
|
|
197
|
+
group.target =
|
|
198
|
+
row.expectedUrl ||
|
|
199
|
+
row.actualUrl ||
|
|
200
|
+
row.expectedCommand ||
|
|
201
|
+
row.actualCommand ||
|
|
202
|
+
"";
|
|
203
|
+
}
|
|
204
|
+
group.rows.push(row);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [...grouped.values()]
|
|
208
|
+
.map((server) => {
|
|
209
|
+
const clients = MCP_CLIENTS.map((client) => {
|
|
210
|
+
const clientRows = server.rows.filter(
|
|
211
|
+
(row) => row.client === client.id,
|
|
212
|
+
);
|
|
213
|
+
if (clientRows.length === 0) return null;
|
|
214
|
+
const present = clientRows.filter(
|
|
215
|
+
(row) => row.status === "present" || row.status === "ok",
|
|
216
|
+
).length;
|
|
217
|
+
let status = "off";
|
|
218
|
+
if (present === clientRows.length) status = "on";
|
|
219
|
+
else if (present > 0) status = "partial";
|
|
220
|
+
return {
|
|
221
|
+
...client,
|
|
222
|
+
status,
|
|
223
|
+
present,
|
|
224
|
+
total: clientRows.length,
|
|
225
|
+
labels: [
|
|
226
|
+
...new Set(clientRows.map((row) => row.label).filter(Boolean)),
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}).filter(Boolean);
|
|
230
|
+
const onCount = clients.filter((client) => client.status === "on").length;
|
|
231
|
+
const partialCount = clients.filter(
|
|
232
|
+
(client) => client.status === "partial",
|
|
233
|
+
).length;
|
|
234
|
+
let status = "off";
|
|
235
|
+
if (clients.length > 0 && onCount === clients.length) status = "on";
|
|
236
|
+
else if (onCount > 0 || partialCount > 0) status = "partial";
|
|
237
|
+
return {
|
|
238
|
+
name: server.name,
|
|
239
|
+
target: server.target,
|
|
240
|
+
status,
|
|
241
|
+
clients,
|
|
242
|
+
client_count: clients.length,
|
|
243
|
+
present_client_count: onCount,
|
|
244
|
+
};
|
|
245
|
+
})
|
|
246
|
+
.sort((a, b) => {
|
|
247
|
+
if (a.name === "tfx-hub") return -1;
|
|
248
|
+
if (b.name === "tfx-hub") return 1;
|
|
249
|
+
return a.name.localeCompare(b.name);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalizeMcpStatus(mcpStatus = {}) {
|
|
254
|
+
const rows = Array.isArray(mcpStatus?.rows)
|
|
255
|
+
? mcpStatus.rows.map(sanitizeMcpRow)
|
|
256
|
+
: [];
|
|
257
|
+
return {
|
|
258
|
+
registry_path: String(
|
|
259
|
+
mcpStatus?.registry_path || mcpStatus?.registryPath || "",
|
|
260
|
+
),
|
|
261
|
+
server_count: Number.isFinite(Number(mcpStatus?.server_count))
|
|
262
|
+
? Number(mcpStatus.server_count)
|
|
263
|
+
: Number.isFinite(Number(mcpStatus?.serverCount))
|
|
264
|
+
? Number(mcpStatus.serverCount)
|
|
265
|
+
: 0,
|
|
266
|
+
rows,
|
|
267
|
+
clients: summarizeMcpClients(rows),
|
|
268
|
+
servers: summarizeMcpServers(rows),
|
|
269
|
+
summary: {
|
|
270
|
+
total: rows.length,
|
|
271
|
+
by_status: countBy(rows, (row) => row.status),
|
|
272
|
+
by_client: countBy(rows, (row) => row.client || row.label),
|
|
273
|
+
},
|
|
274
|
+
...(mcpStatus?.error ? { error: String(mcpStatus.error) } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeHub(hub = {}) {
|
|
279
|
+
const id = String(hub?.id ?? hub?.sessionId ?? hub?.session_id ?? "");
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
pid: Number.isFinite(Number(hub?.pid)) ? Number(hub.pid) : null,
|
|
283
|
+
port: Number.isFinite(Number(hub?.port)) ? Number(hub.port) : null,
|
|
284
|
+
url: String(hub?.url || ""),
|
|
285
|
+
projectRoot: String(hub?.projectRoot || hub?.project_root || ""),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function numericPid(value) {
|
|
290
|
+
const text = String(value ?? "").trim();
|
|
291
|
+
if (!text) return null;
|
|
292
|
+
const pid = Number(text);
|
|
293
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeRuntimeStatus(runtimeStatus = {}) {
|
|
297
|
+
const processes = Array.isArray(runtimeStatus?.processes)
|
|
298
|
+
? runtimeStatus.processes.map((process) => ({
|
|
299
|
+
client: String(process?.client || ""),
|
|
300
|
+
sessionId: String(process?.sessionId || ""),
|
|
301
|
+
source: String(process?.source || ""),
|
|
302
|
+
conversationId: String(process?.conversationId || ""),
|
|
303
|
+
focusable: process?.focusable !== false,
|
|
304
|
+
pid: numericPid(process?.pid),
|
|
305
|
+
ppid: Number.isFinite(Number(process?.ppid))
|
|
306
|
+
? Number(process.ppid)
|
|
307
|
+
: null,
|
|
308
|
+
phase:
|
|
309
|
+
typeof process?.phase === "string"
|
|
310
|
+
? process.phase
|
|
311
|
+
: process?.source === "conversation"
|
|
312
|
+
? "idle"
|
|
313
|
+
: "active",
|
|
314
|
+
elapsed: String(process?.elapsed || ""),
|
|
315
|
+
command: String(process?.command || ""),
|
|
316
|
+
cwd: String(process?.cwd || ""),
|
|
317
|
+
taskSummary: String(process?.taskSummary || process?.command || ""),
|
|
318
|
+
}))
|
|
319
|
+
: [];
|
|
320
|
+
return {
|
|
321
|
+
processes: processes.filter(
|
|
322
|
+
(process) =>
|
|
323
|
+
process.client &&
|
|
324
|
+
(process.pid || process.sessionId || process.conversationId),
|
|
325
|
+
),
|
|
326
|
+
summary: runtimeStatus?.summary || {
|
|
327
|
+
total: processes.length,
|
|
328
|
+
clients: [],
|
|
329
|
+
},
|
|
330
|
+
...(runtimeStatus?.error ? { error: String(runtimeStatus.error) } : {}),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function sessionsToLiveOverlay(sessions = []) {
|
|
335
|
+
return sessions
|
|
336
|
+
.filter((session) => session.status !== "stale")
|
|
337
|
+
.map((session) => ({
|
|
338
|
+
sessionId: session.sessionId,
|
|
339
|
+
host: session.host,
|
|
340
|
+
agent_id: session.agent_id,
|
|
341
|
+
agent: session.agent,
|
|
342
|
+
phase: session.status,
|
|
343
|
+
started_at: session.started_at,
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function runtimeToLiveOverlay(runtime) {
|
|
348
|
+
return runtime.processes.map((process) => ({
|
|
349
|
+
sessionId:
|
|
350
|
+
process.sessionId ||
|
|
351
|
+
(process.pid
|
|
352
|
+
? `${process.client}-${process.pid}`
|
|
353
|
+
: `${process.client}-${process.conversationId || "session"}`),
|
|
354
|
+
host: "local",
|
|
355
|
+
agent_id: process.client,
|
|
356
|
+
agent: agentMeta(process.client),
|
|
357
|
+
phase: process.phase || "active",
|
|
358
|
+
pid: process.pid,
|
|
359
|
+
command: process.command,
|
|
360
|
+
elapsed: process.elapsed,
|
|
361
|
+
cwd: process.cwd,
|
|
362
|
+
taskSummary: process.taskSummary || process.command || `PID ${process.pid}`,
|
|
363
|
+
source: process.source,
|
|
364
|
+
conversationId: process.conversationId,
|
|
365
|
+
focusable: process.focusable,
|
|
366
|
+
started_at: null,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeLiveSession(session = {}) {
|
|
371
|
+
const agentId = inferAgentId(session);
|
|
372
|
+
return {
|
|
373
|
+
...session,
|
|
374
|
+
sessionId: String(session.sessionId || ""),
|
|
375
|
+
host: typeof session.host === "string" ? session.host : "local",
|
|
376
|
+
agent_id: agentId || null,
|
|
377
|
+
agent: session.agent || agentMeta(agentId),
|
|
378
|
+
phase:
|
|
379
|
+
typeof session.phase === "string"
|
|
380
|
+
? session.phase
|
|
381
|
+
: typeof session.status === "string"
|
|
382
|
+
? session.status
|
|
383
|
+
: "active",
|
|
384
|
+
started_at: toIsoTime(
|
|
385
|
+
session.started_at ?? session.startedAt ?? session.lastHeartbeat,
|
|
386
|
+
),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeCtoStatus(ctoStatus = {}, sessions = [], runtime = {}) {
|
|
391
|
+
const explicitLive = Array.isArray(ctoStatus?.live_sessions)
|
|
392
|
+
? ctoStatus.live_sessions
|
|
393
|
+
: null;
|
|
394
|
+
const sessionLive = sessionsToLiveOverlay(sessions);
|
|
395
|
+
const runtimeLive = runtimeToLiveOverlay(runtime);
|
|
396
|
+
const overlayLive =
|
|
397
|
+
explicitLive && explicitLive.length > 0
|
|
398
|
+
? explicitLive
|
|
399
|
+
: sessionLive.length > 0
|
|
400
|
+
? sessionLive
|
|
401
|
+
: runtimeLive;
|
|
402
|
+
const liveSessions =
|
|
403
|
+
runtimeLive.length > overlayLive.length ? runtimeLive : overlayLive;
|
|
404
|
+
const normalizedLiveSessions = liveSessions
|
|
405
|
+
.map(normalizeLiveSession)
|
|
406
|
+
.filter((session) => session.sessionId);
|
|
407
|
+
const activeShards = Array.isArray(ctoStatus?.active_shards)
|
|
408
|
+
? ctoStatus.active_shards
|
|
409
|
+
: [];
|
|
410
|
+
return {
|
|
411
|
+
schema_version: String(ctoStatus?.schema_version || "cto-lake.v1"),
|
|
412
|
+
generated_at: ctoStatus?.generated_at || null,
|
|
413
|
+
live_sessions: normalizedLiveSessions,
|
|
414
|
+
active_shards: activeShards,
|
|
415
|
+
summary: {
|
|
416
|
+
live_session_count: normalizedLiveSessions.length,
|
|
417
|
+
active_shard_count: activeShards.length,
|
|
418
|
+
tray_session_count: sessions.length,
|
|
419
|
+
tray_sessions_by_status: countBy(sessions, (session) => session.status),
|
|
420
|
+
},
|
|
421
|
+
...(ctoStatus?.error ? { error: String(ctoStatus.error) } : {}),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function buildTrayStatePayload({
|
|
426
|
+
hub = {},
|
|
427
|
+
qos = {},
|
|
428
|
+
synapseSnapshot = {},
|
|
429
|
+
broker = [],
|
|
430
|
+
ctoStatus = {},
|
|
431
|
+
mcpStatus = {},
|
|
432
|
+
runtimeStatus = {},
|
|
433
|
+
now = Date.now(),
|
|
434
|
+
} = {}) {
|
|
435
|
+
const sessions = normalizeTraySessions(synapseSnapshot);
|
|
436
|
+
const runtime = normalizeRuntimeStatus(runtimeStatus);
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
hub: normalizeHub(hub),
|
|
440
|
+
qos,
|
|
441
|
+
sessions,
|
|
442
|
+
cto: normalizeCtoStatus(ctoStatus, sessions, runtime),
|
|
443
|
+
mcp: normalizeMcpStatus(mcpStatus),
|
|
444
|
+
runtime,
|
|
445
|
+
broker: Array.isArray(broker) ? broker : [],
|
|
446
|
+
ts: now,
|
|
447
|
+
};
|
|
448
|
+
}
|
package/hub/tray.mjs
CHANGED
|
@@ -4,16 +4,71 @@ import _SysTrayModule from "systray2";
|
|
|
4
4
|
|
|
5
5
|
const SysTray = _SysTrayModule.default || _SysTrayModule;
|
|
6
6
|
|
|
7
|
-
import { exec } from "node:child_process";
|
|
7
|
+
import { exec, execFileSync, spawn } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
|
|
12
|
+
import { IS_MAC, IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
|
|
13
|
+
import { ensureHubForTray } from "./tray-lifecycle.mjs";
|
|
13
14
|
|
|
14
15
|
const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
15
16
|
const DEFAULT_HUB_PORT = "27888";
|
|
16
17
|
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function collectMacTrayProcesses(
|
|
23
|
+
psOutput = "",
|
|
24
|
+
{
|
|
25
|
+
scriptPath = fileURLToPath(import.meta.url),
|
|
26
|
+
currentPid = process.pid,
|
|
27
|
+
} = {},
|
|
28
|
+
) {
|
|
29
|
+
const target = String(scriptPath || "");
|
|
30
|
+
if (!target) return [];
|
|
31
|
+
const ownPid = Number(currentPid);
|
|
32
|
+
return String(psOutput)
|
|
33
|
+
.split(/\r?\n/u)
|
|
34
|
+
.flatMap((line) => {
|
|
35
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/u);
|
|
36
|
+
if (!match) return [];
|
|
37
|
+
const pid = Number.parseInt(match[1], 10);
|
|
38
|
+
const ppid = Number.parseInt(match[2], 10);
|
|
39
|
+
const command = match[3].trim();
|
|
40
|
+
if (!Number.isFinite(pid) || pid === ownPid) return [];
|
|
41
|
+
if (!command.includes(target)) return [];
|
|
42
|
+
if (!/\bnode(?:\s|$)|\/node(?:\s|$)/u.test(command)) return [];
|
|
43
|
+
return [{ pid, ppid, command }];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function reapExistingMacTrayProcesses({
|
|
48
|
+
scriptPath = fileURLToPath(import.meta.url),
|
|
49
|
+
currentPid = process.pid,
|
|
50
|
+
execFileSyncFn = execFileSync,
|
|
51
|
+
killFn = process.kill,
|
|
52
|
+
} = {}) {
|
|
53
|
+
let output = "";
|
|
54
|
+
try {
|
|
55
|
+
output = execFileSyncFn("ps", ["-axo", "pid,ppid,command"], {
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
timeout: 1000,
|
|
58
|
+
maxBuffer: 1024 * 1024,
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const existing = collectMacTrayProcesses(output, { scriptPath, currentPid });
|
|
64
|
+
for (const processInfo of existing) {
|
|
65
|
+
try {
|
|
66
|
+
killFn(processInfo.pid, "SIGTERM");
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return existing;
|
|
70
|
+
}
|
|
71
|
+
|
|
17
72
|
function getHubBaseUrl() {
|
|
18
73
|
if (process.env.TFX_HUB_URL)
|
|
19
74
|
return process.env.TFX_HUB_URL.replace(/\/+$/, "");
|
|
@@ -354,8 +409,42 @@ async function shutdown(reason = "shutdown") {
|
|
|
354
409
|
}
|
|
355
410
|
|
|
356
411
|
export async function startTray() {
|
|
412
|
+
if (IS_MAC) {
|
|
413
|
+
const trayScript = fileURLToPath(import.meta.url);
|
|
414
|
+
const port = process.env.TFX_HUB_PORT || DEFAULT_HUB_PORT;
|
|
415
|
+
const serverPath = join(dirname(trayScript), "server.mjs");
|
|
416
|
+
await ensureHubForTray({ port, serverPath });
|
|
417
|
+
const reaped = reapExistingMacTrayProcesses({ scriptPath: trayScript });
|
|
418
|
+
if (reaped.length > 0) await sleep(350);
|
|
419
|
+
const swiftScript = join(dirname(trayScript), "mac-tray.swift");
|
|
420
|
+
|
|
421
|
+
// Spawn swift script detached or as a child
|
|
422
|
+
const trayProc = spawn("swift", [swiftScript, port], {
|
|
423
|
+
stdio: "ignore",
|
|
424
|
+
detached: false,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
trayProc.on("error", (err) =>
|
|
428
|
+
console.error("[tfx-tray-mac] start failed:", err),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
trayProc.on("exit", (code) => {
|
|
432
|
+
console.error(`[tfx-tray-mac] swift script exited (code ${code})`);
|
|
433
|
+
process.exit(typeof code === "number" ? code : 1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const cleanup = () => trayProc.kill();
|
|
437
|
+
process.on("SIGINT", cleanup);
|
|
438
|
+
process.on("SIGTERM", cleanup);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
systray: null,
|
|
442
|
+
stop: () => trayProc.kill(),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
357
446
|
if (!IS_WINDOWS) {
|
|
358
|
-
throw new Error("tray command is only supported on Windows.");
|
|
447
|
+
throw new Error("systray2 tray command is only supported on Windows.");
|
|
359
448
|
}
|
|
360
449
|
|
|
361
450
|
systray = new SysTray({
|
|
@@ -16,7 +16,11 @@ function resolveCandidatePath(candidate, cwd = process.cwd()) {
|
|
|
16
16
|
return existsSync(normalized) ? normalized : null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function resolveRouteScript(
|
|
19
|
+
function resolveRouteScript(
|
|
20
|
+
explicitPath,
|
|
21
|
+
cwd = process.cwd(),
|
|
22
|
+
env = process.env,
|
|
23
|
+
) {
|
|
20
24
|
const candidates = [
|
|
21
25
|
explicitPath,
|
|
22
26
|
env.TFX_DELEGATOR_ROUTE_SCRIPT,
|
|
@@ -57,10 +61,15 @@ export class AntigravityRouteWorker {
|
|
|
57
61
|
constructor(options = {}) {
|
|
58
62
|
this.cwd = options.cwd || process.cwd();
|
|
59
63
|
this.env = { ...process.env, ...(options.env || {}) };
|
|
60
|
-
this.routeScript = resolveRouteScript(
|
|
64
|
+
this.routeScript = resolveRouteScript(
|
|
65
|
+
options.routeScript,
|
|
66
|
+
this.cwd,
|
|
67
|
+
this.env,
|
|
68
|
+
);
|
|
61
69
|
this.bashCommand = options.bashCommand || this.env.BASH_BIN || "bash";
|
|
62
70
|
this.timeoutMs =
|
|
63
|
-
Number.isFinite(Number(options.timeoutMs)) &&
|
|
71
|
+
Number.isFinite(Number(options.timeoutMs)) &&
|
|
72
|
+
Number(options.timeoutMs) > 0
|
|
64
73
|
? Number(options.timeoutMs)
|
|
65
74
|
: DEFAULT_TIMEOUT_MS;
|
|
66
75
|
this.children = new Set();
|
|
@@ -89,7 +98,8 @@ export class AntigravityRouteWorker {
|
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
const timeoutMs =
|
|
92
|
-
Number.isFinite(Number(options.timeoutMs)) &&
|
|
101
|
+
Number.isFinite(Number(options.timeoutMs)) &&
|
|
102
|
+
Number(options.timeoutMs) > 0
|
|
93
103
|
? Number(options.timeoutMs)
|
|
94
104
|
: this.timeoutMs;
|
|
95
105
|
const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
@@ -29,11 +29,8 @@ if (!mcpFilterPath) {
|
|
|
29
29
|
`mcp-filter.mjs not found. candidates: ${MCP_FILTER_CANDIDATES.join(", ")}`,
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
getCodexMcpConfig,
|
|
35
|
-
SUPPORTED_MCP_PROFILES,
|
|
36
|
-
} = await import(pathToFileURL(mcpFilterPath).href);
|
|
32
|
+
const { buildPromptHint, getCodexMcpConfig, SUPPORTED_MCP_PROFILES } =
|
|
33
|
+
await import(pathToFileURL(mcpFilterPath).href);
|
|
37
34
|
const SERVER_INFO = { name: "triflux-delegator", version: "1.0.0" };
|
|
38
35
|
const DEFAULT_CONTEXT_BYTES = 32 * 1024;
|
|
39
36
|
const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
|
package/hub/workers/factory.mjs
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// existing `requestJson` helper. Callers can override by passing their own
|
|
15
15
|
// `publishCallback` or swap the transport with `requestJsonFn`.
|
|
16
16
|
|
|
17
|
-
import { ClaudeWorker } from "./claude-worker.mjs";
|
|
18
17
|
import { AntigravityRouteWorker } from "./antigravity-route-worker.mjs";
|
|
18
|
+
import { ClaudeWorker } from "./claude-worker.mjs";
|
|
19
19
|
import { CodexAppServerWorker } from "./codex-app-server-worker.mjs";
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -102,7 +102,8 @@ function detectAntigravityCategory(error) {
|
|
|
102
102
|
function buildAntigravityErrorInfo(error, attempts) {
|
|
103
103
|
const category = detectAntigravityCategory(error);
|
|
104
104
|
const retryable = isAntigravityRetryable(error);
|
|
105
|
-
let recovery =
|
|
105
|
+
let recovery =
|
|
106
|
+
"Retry the Antigravity route worker after correcting the issue.";
|
|
106
107
|
|
|
107
108
|
if (category === "auth") {
|
|
108
109
|
recovery = "Refresh the Antigravity authentication state and retry.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triflux/remote",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.33.0",
|
|
4
4
|
"description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "hub/index.mjs",
|
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
19
|
+
"ajv": "^8.18.0",
|
|
19
20
|
"better-sqlite3": "^12.10.0",
|
|
20
|
-
"
|
|
21
|
+
"pino": "^10.3.1",
|
|
22
|
+
"pino-pretty": "^13.1.3",
|
|
23
|
+
"systray2": "^2.1.4",
|
|
24
|
+
"zod": "^4.0.0"
|
|
21
25
|
},
|
|
22
26
|
"files": [
|
|
23
27
|
"hub",
|
|
28
|
+
"cto",
|
|
24
29
|
"scripts/lib"
|
|
25
30
|
],
|
|
26
31
|
"keywords": [
|