u-foo 2.4.9 → 2.4.10
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/package.json
CHANGED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const net = require("net");
|
|
5
|
+
const EventBus = require("../../coordination/bus");
|
|
6
|
+
const { normalizeReportInput } = require("../../coordination/report/store");
|
|
7
|
+
const { enqueueAgentReport } = require("./reportControlBus");
|
|
8
|
+
const { isRunning, socketPath } = require("./index");
|
|
9
|
+
const { IPC_REQUEST_TYPES } = require("../contracts/eventContract");
|
|
10
|
+
const {
|
|
11
|
+
applyProjectNicknamePrefix,
|
|
12
|
+
checkAndCleanupNickname,
|
|
13
|
+
} = require("./nicknameScope");
|
|
14
|
+
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeBusAgentType(agentType = "") {
|
|
20
|
+
const value = String(agentType || "").trim().toLowerCase();
|
|
21
|
+
if (!value) return "mcp-agent";
|
|
22
|
+
if (value === "claude") return "claude-code";
|
|
23
|
+
if (value === "ucode" || value === "ufoo") return "ufoo-code";
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureBusLoaded(projectRoot) {
|
|
28
|
+
const bus = new EventBus(projectRoot);
|
|
29
|
+
bus.ensureBus();
|
|
30
|
+
bus.loadBusData();
|
|
31
|
+
return bus;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertSubscriberExists(bus, subscriber) {
|
|
35
|
+
const meta = bus.subscriberManager.getSubscriber(subscriber);
|
|
36
|
+
if (!meta) {
|
|
37
|
+
const err = new Error(`subscriber not found: ${subscriber}`);
|
|
38
|
+
err.code = "subscriber_not_found";
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return meta;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveSubscriberArg(args = {}) {
|
|
45
|
+
const subscriber = String(args.subscriber || args.source || "").trim();
|
|
46
|
+
if (!subscriber) {
|
|
47
|
+
const err = new Error("subscriber is required");
|
|
48
|
+
err.code = "invalid_subscriber";
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
return subscriber;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createSessionId() {
|
|
55
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createCryptoSessionId() {
|
|
59
|
+
return crypto.randomBytes(4).toString("hex");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function notifyDaemonRefresh(projectRoot) {
|
|
63
|
+
if (!isRunning(projectRoot)) return;
|
|
64
|
+
const sock = socketPath(projectRoot);
|
|
65
|
+
try {
|
|
66
|
+
const client = net.createConnection(sock, () => {
|
|
67
|
+
client.write(`${JSON.stringify({ type: IPC_REQUEST_TYPES.REFRESH_STATUS })}\n`);
|
|
68
|
+
client.end();
|
|
69
|
+
});
|
|
70
|
+
client.on("error", () => {});
|
|
71
|
+
} catch {
|
|
72
|
+
// fire-and-forget
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function registerAgentFull(projectRoot, args = {}, options = {}) {
|
|
77
|
+
const {
|
|
78
|
+
validateParentPid = false,
|
|
79
|
+
checkNicknameConflicts = false,
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
const agentType = normalizeBusAgentType(args.agent_type || args.agentType || "mcp-agent");
|
|
83
|
+
const nickname = String(args.nickname || "").trim();
|
|
84
|
+
const launchMode = String(args.launch_mode || args.launchMode || "mcp").trim();
|
|
85
|
+
const capabilities = args.capabilities && typeof args.capabilities === "object"
|
|
86
|
+
? args.capabilities
|
|
87
|
+
: null;
|
|
88
|
+
const hostCapabilities = args.hostCapabilities && typeof args.hostCapabilities === "object"
|
|
89
|
+
? args.hostCapabilities
|
|
90
|
+
: capabilities;
|
|
91
|
+
|
|
92
|
+
// Session ID: explicit > reuse > generate
|
|
93
|
+
let sessionId;
|
|
94
|
+
const explicitSessionId = String(args.session_id || args.sessionId || "").trim();
|
|
95
|
+
const reuseSession = args.reuseSession && typeof args.reuseSession === "object"
|
|
96
|
+
? args.reuseSession
|
|
97
|
+
: null;
|
|
98
|
+
const reuseSessionId = typeof reuseSession?.sessionId === "string"
|
|
99
|
+
? reuseSession.sessionId.trim() : "";
|
|
100
|
+
const reuseSubscriberId = typeof reuseSession?.subscriberId === "string"
|
|
101
|
+
? reuseSession.subscriberId.trim() : "";
|
|
102
|
+
const reuseProviderSessionId = typeof reuseSession?.providerSessionId === "string"
|
|
103
|
+
? reuseSession.providerSessionId.trim() : "";
|
|
104
|
+
|
|
105
|
+
if (explicitSessionId) {
|
|
106
|
+
sessionId = explicitSessionId;
|
|
107
|
+
} else if (reuseSessionId && reuseSubscriberId === `${agentType}:${reuseSessionId}`) {
|
|
108
|
+
sessionId = reuseSessionId;
|
|
109
|
+
} else {
|
|
110
|
+
sessionId = validateParentPid ? createCryptoSessionId() : createSessionId();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// parentPid validation
|
|
114
|
+
const parentPid = Number.parseInt(args.parentPid, 10);
|
|
115
|
+
if (validateParentPid) {
|
|
116
|
+
if (!Number.isFinite(parentPid) || parentPid <= 0) {
|
|
117
|
+
const err = new Error("register_agent requires valid parentPid");
|
|
118
|
+
err.code = "invalid_parent_pid";
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Nickname scope and conflict check
|
|
124
|
+
let finalNickname = nickname;
|
|
125
|
+
let scopedNickname = nickname
|
|
126
|
+
? applyProjectNicknamePrefix(projectRoot, nickname, { agentType })
|
|
127
|
+
: "";
|
|
128
|
+
if (checkNicknameConflicts && finalNickname) {
|
|
129
|
+
const nickCheck = checkAndCleanupNickname(projectRoot, finalNickname, {
|
|
130
|
+
tty: String(args.tty || ""),
|
|
131
|
+
agentType,
|
|
132
|
+
scopedNickname,
|
|
133
|
+
});
|
|
134
|
+
if (nickCheck.existing) {
|
|
135
|
+
finalNickname = "";
|
|
136
|
+
scopedNickname = "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Bus join
|
|
141
|
+
const joinOptions = {
|
|
142
|
+
parentPid: Number.isFinite(parentPid) && parentPid > 0 ? parentPid : process.pid,
|
|
143
|
+
launchMode,
|
|
144
|
+
tmuxPane: String(args.tmuxPane || ""),
|
|
145
|
+
tty: String(args.tty || ""),
|
|
146
|
+
hostInjectSock: String(args.hostInjectSock || ""),
|
|
147
|
+
hostDaemonSock: String(args.hostDaemonSock || ""),
|
|
148
|
+
hostName: String(args.host_name || args.hostName || "ufoo-mcp"),
|
|
149
|
+
hostSessionId: String(args.hostSessionId || `mcp-${process.pid}`),
|
|
150
|
+
hostCapabilities: hostCapabilities,
|
|
151
|
+
scopedNickname: scopedNickname || String(args.scoped_nickname || args.scopedNickname || finalNickname || "").trim(),
|
|
152
|
+
};
|
|
153
|
+
if (args.skipSessionResolve) joinOptions.skipSessionResolve = true;
|
|
154
|
+
if (reuseSessionId) joinOptions.reuseSessionId = reuseSessionId;
|
|
155
|
+
if (reuseProviderSessionId) joinOptions.reuseProviderSessionId = reuseProviderSessionId;
|
|
156
|
+
|
|
157
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
158
|
+
const result = await bus.subscriberManager.join(sessionId, agentType, finalNickname, joinOptions);
|
|
159
|
+
const subscriber = result.subscriber;
|
|
160
|
+
if (finalNickname) {
|
|
161
|
+
bus.subscriberManager.rename(subscriber, finalNickname, "ufoo-agent", { scopedNickname });
|
|
162
|
+
}
|
|
163
|
+
const meta = bus.subscriberManager.getSubscriber(subscriber) || {};
|
|
164
|
+
meta.activity_state = String(args.activity_state || "ready");
|
|
165
|
+
meta.activity_since = nowIso();
|
|
166
|
+
meta.mcp_bridge = !validateParentPid;
|
|
167
|
+
if (hostCapabilities) meta.mcp_capabilities = hostCapabilities;
|
|
168
|
+
bus.saveBusData();
|
|
169
|
+
notifyDaemonRefresh(projectRoot);
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
project_root: projectRoot,
|
|
173
|
+
subscriber_id: subscriber,
|
|
174
|
+
subscriber,
|
|
175
|
+
session_id: sessionId,
|
|
176
|
+
agent_type: agentType,
|
|
177
|
+
nickname: meta.nickname || result.nickname || finalNickname || "",
|
|
178
|
+
scoped_nickname: meta.scoped_nickname || result.scopedNickname || scopedNickname || "",
|
|
179
|
+
launch_mode: launchMode,
|
|
180
|
+
reuseProviderSessionId,
|
|
181
|
+
skipSessionResolve: !!args.skipSessionResolve,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function registerAgent(projectRoot, args = {}) {
|
|
186
|
+
return registerAgentFull(projectRoot, args, {
|
|
187
|
+
validateParentPid: false,
|
|
188
|
+
checkNicknameConflicts: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function heartbeatAgent(projectRoot, args = {}) {
|
|
193
|
+
const subscriber = resolveSubscriberArg(args);
|
|
194
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
195
|
+
const meta = assertSubscriberExists(bus, subscriber);
|
|
196
|
+
bus.subscriberManager.updateLastSeen(subscriber);
|
|
197
|
+
meta.status = "active";
|
|
198
|
+
bus.saveBusData();
|
|
199
|
+
notifyDaemonRefresh(projectRoot);
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
project_root: projectRoot,
|
|
203
|
+
subscriber,
|
|
204
|
+
last_seen: meta.last_seen,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function publishActivityState(projectRoot, args = {}) {
|
|
209
|
+
const subscriber = resolveSubscriberArg(args);
|
|
210
|
+
const activityState = String(args.activity_state || args.activityState || "").trim();
|
|
211
|
+
if (!activityState) {
|
|
212
|
+
const err = new Error("activity_state is required");
|
|
213
|
+
err.code = "invalid_activity_state";
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
217
|
+
const meta = assertSubscriberExists(bus, subscriber);
|
|
218
|
+
bus.subscriberManager.updateLastSeen(subscriber);
|
|
219
|
+
meta.status = "active";
|
|
220
|
+
meta.activity_state = activityState;
|
|
221
|
+
meta.activity_detail = String(args.detail || "").trim();
|
|
222
|
+
meta.activity_since = String(args.since || "").trim() || nowIso();
|
|
223
|
+
bus.saveBusData();
|
|
224
|
+
notifyDaemonRefresh(projectRoot);
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
project_root: projectRoot,
|
|
228
|
+
subscriber,
|
|
229
|
+
activity_state: meta.activity_state,
|
|
230
|
+
activity_detail: meta.activity_detail,
|
|
231
|
+
activity_since: meta.activity_since,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function updateAgentMetadata(projectRoot, args = {}) {
|
|
236
|
+
const subscriber = resolveSubscriberArg(args);
|
|
237
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
238
|
+
const meta = assertSubscriberExists(bus, subscriber);
|
|
239
|
+
const nickname = String(args.nickname || "").trim();
|
|
240
|
+
if (nickname) {
|
|
241
|
+
await bus.subscriberManager.rename(subscriber, nickname);
|
|
242
|
+
}
|
|
243
|
+
const metadata = args.metadata && typeof args.metadata === "object" ? args.metadata : {};
|
|
244
|
+
if (Object.keys(metadata).length > 0) {
|
|
245
|
+
meta.mcp_metadata = {
|
|
246
|
+
...(meta.mcp_metadata && typeof meta.mcp_metadata === "object" ? meta.mcp_metadata : {}),
|
|
247
|
+
...metadata,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
bus.subscriberManager.updateLastSeen(subscriber);
|
|
251
|
+
bus.saveBusData();
|
|
252
|
+
notifyDaemonRefresh(projectRoot);
|
|
253
|
+
const nextMeta = bus.subscriberManager.getSubscriber(subscriber) || meta;
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
project_root: projectRoot,
|
|
257
|
+
subscriber,
|
|
258
|
+
nickname: nextMeta.nickname || "",
|
|
259
|
+
scoped_nickname: nextMeta.scoped_nickname || nextMeta.nickname || "",
|
|
260
|
+
metadata: nextMeta.mcp_metadata || {},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function pollInbox(projectRoot, args = {}) {
|
|
265
|
+
const subscriber = resolveSubscriberArg(args);
|
|
266
|
+
const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0
|
|
267
|
+
? Math.floor(Number(args.limit))
|
|
268
|
+
: 50;
|
|
269
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
270
|
+
assertSubscriberExists(bus, subscriber);
|
|
271
|
+
bus.subscriberManager.updateLastSeen(subscriber);
|
|
272
|
+
bus.saveBusData();
|
|
273
|
+
const pending = await bus.messageManager.check(subscriber);
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
project_root: projectRoot,
|
|
277
|
+
subscriber,
|
|
278
|
+
count: pending.length,
|
|
279
|
+
messages: pending.slice(0, limit),
|
|
280
|
+
truncated: pending.length > limit,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function reportAgentStatus(projectRoot, args = {}) {
|
|
285
|
+
const subscriber = resolveSubscriberArg(args);
|
|
286
|
+
const report = normalizeReportInput({
|
|
287
|
+
...args,
|
|
288
|
+
agent_id: subscriber,
|
|
289
|
+
source: "mcp",
|
|
290
|
+
});
|
|
291
|
+
const queued = await enqueueAgentReport(projectRoot, report, { publisher: subscriber });
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
project_root: projectRoot,
|
|
295
|
+
status: "queued",
|
|
296
|
+
request_id: queued.request_id,
|
|
297
|
+
report,
|
|
298
|
+
queued,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function unregisterAgent(projectRoot, args = {}) {
|
|
303
|
+
const subscriber = resolveSubscriberArg(args);
|
|
304
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
305
|
+
const ok = await bus.subscriberManager.leave(subscriber);
|
|
306
|
+
bus.saveBusData();
|
|
307
|
+
notifyDaemonRefresh(projectRoot);
|
|
308
|
+
return {
|
|
309
|
+
ok,
|
|
310
|
+
project_root: projectRoot,
|
|
311
|
+
subscriber,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
normalizeBusAgentType,
|
|
317
|
+
ensureBusLoaded,
|
|
318
|
+
assertSubscriberExists,
|
|
319
|
+
resolveSubscriberArg,
|
|
320
|
+
createSessionId,
|
|
321
|
+
notifyDaemonRefresh,
|
|
322
|
+
registerAgentFull,
|
|
323
|
+
registerAgent,
|
|
324
|
+
heartbeatAgent,
|
|
325
|
+
publishActivityState,
|
|
326
|
+
updateAgentMetadata,
|
|
327
|
+
pollInbox,
|
|
328
|
+
reportAgentStatus,
|
|
329
|
+
unregisterAgent,
|
|
330
|
+
};
|
|
@@ -40,6 +40,7 @@ const {
|
|
|
40
40
|
applyProjectNicknamePrefix,
|
|
41
41
|
resolveDisplayNickname,
|
|
42
42
|
resolveScopedNickname,
|
|
43
|
+
checkAndCleanupNickname,
|
|
43
44
|
} = require("./nicknameScope");
|
|
44
45
|
const { resolveNodeExecutable } = require("../process/nodeExecutable");
|
|
45
46
|
|
|
@@ -474,57 +475,6 @@ async function waitForNewSubscriber(projectRoot, agentType, existing, timeoutMs
|
|
|
474
475
|
return null;
|
|
475
476
|
}
|
|
476
477
|
|
|
477
|
-
function checkAndCleanupNickname(projectRoot, nickname, { tty = "", agentType = "", scopedNickname = "" } = {}) {
|
|
478
|
-
const conflictNickname = scopedNickname || applyProjectNicknamePrefix(projectRoot, nickname, {
|
|
479
|
-
agentType,
|
|
480
|
-
force: true,
|
|
481
|
-
});
|
|
482
|
-
if (!conflictNickname) return { existing: null, cleaned: false };
|
|
483
|
-
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
484
|
-
try {
|
|
485
|
-
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
486
|
-
const entries = Object.entries(bus.agents || {})
|
|
487
|
-
.filter(([, meta]) => {
|
|
488
|
-
const candidate = resolveScopedNickname(projectRoot, meta);
|
|
489
|
-
return meta && candidate === conflictNickname;
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
if (entries.length === 0) {
|
|
493
|
-
return { existing: null, cleaned: false };
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Check for active agent with same nickname
|
|
497
|
-
const activeAgent = entries.find(([, meta]) => meta.status === "active");
|
|
498
|
-
if (activeAgent) {
|
|
499
|
-
const [existingId, existingMeta] = activeAgent;
|
|
500
|
-
// Allow takeover when the existing holder is a pre-registered stub
|
|
501
|
-
// (same agent type, no TTY) or occupies the same TTY — the new
|
|
502
|
-
// registration is the real agent replacing the placeholder.
|
|
503
|
-
const sameType = agentType && existingMeta.agent_type === agentType;
|
|
504
|
-
// A stub is a pre-registered entry with no TTY AND no meaningful activity
|
|
505
|
-
// state. Internal-mode agents also lack a TTY but will have activity_state
|
|
506
|
-
// set once they start working — don't evict those.
|
|
507
|
-
const isStub = sameType && !existingMeta.tty && !existingMeta.activity_state;
|
|
508
|
-
const sameTty = tty && existingMeta.tty === tty;
|
|
509
|
-
if (isStub || sameTty) {
|
|
510
|
-
delete bus.agents[existingId];
|
|
511
|
-
fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
|
|
512
|
-
return { existing: null, cleaned: true };
|
|
513
|
-
}
|
|
514
|
-
return { existing: existingId, cleaned: false };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Clean up offline agents with same nickname
|
|
518
|
-
for (const [agentId] of entries) {
|
|
519
|
-
delete bus.agents[agentId];
|
|
520
|
-
}
|
|
521
|
-
fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
|
|
522
|
-
return { existing: null, cleaned: true };
|
|
523
|
-
} catch {
|
|
524
|
-
return { existing: null, cleaned: false };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
478
|
function resolveSubscriberNickname(projectRoot, subscriberId) {
|
|
529
479
|
if (!subscriberId) return "";
|
|
530
480
|
try {
|
|
@@ -2331,19 +2281,10 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2331
2281
|
return;
|
|
2332
2282
|
}
|
|
2333
2283
|
if (req.type === IPC_REQUEST_TYPES.REGISTER_AGENT) {
|
|
2334
|
-
// Manual agent launch requests daemon to register it
|
|
2335
2284
|
const {
|
|
2336
2285
|
agentType,
|
|
2337
2286
|
nickname,
|
|
2338
2287
|
parentPid,
|
|
2339
|
-
launchMode,
|
|
2340
|
-
tmuxPane,
|
|
2341
|
-
tty,
|
|
2342
|
-
hostInjectSock,
|
|
2343
|
-
hostDaemonSock,
|
|
2344
|
-
hostName,
|
|
2345
|
-
hostSessionId,
|
|
2346
|
-
hostCapabilities,
|
|
2347
2288
|
skipSessionResolve,
|
|
2348
2289
|
} = req;
|
|
2349
2290
|
if (!agentType) {
|
|
@@ -2357,85 +2298,22 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2357
2298
|
return;
|
|
2358
2299
|
}
|
|
2359
2300
|
try {
|
|
2360
|
-
const
|
|
2361
|
-
const
|
|
2362
|
-
|
|
2363
|
-
:
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
:
|
|
2367
|
-
|
|
2368
|
-
? requestedReuse.subscriberId.trim()
|
|
2369
|
-
: "";
|
|
2370
|
-
const reuseProviderSessionId = typeof requestedReuse?.providerSessionId === "string"
|
|
2371
|
-
? requestedReuse.providerSessionId.trim()
|
|
2372
|
-
: "";
|
|
2373
|
-
|
|
2374
|
-
let sessionId = crypto.randomBytes(4).toString("hex");
|
|
2375
|
-
let subscriberId = `${agentType}:${sessionId}`;
|
|
2376
|
-
if (reuseSessionId && reuseSubscriberId === `${agentType}:${reuseSessionId}`) {
|
|
2377
|
-
sessionId = reuseSessionId;
|
|
2378
|
-
subscriberId = reuseSubscriberId;
|
|
2379
|
-
} else if (reuseSessionId || reuseSubscriberId) {
|
|
2380
|
-
log(`register_agent ignored invalid reuseSession for ${agentType}`);
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
// Daemon registers the agent in bus
|
|
2384
|
-
const eventBus = new EventBus(projectRoot);
|
|
2385
|
-
await eventBus.init();
|
|
2386
|
-
eventBus.loadBusData();
|
|
2387
|
-
const parsedParentPid = Number.parseInt(parentPid, 10);
|
|
2388
|
-
if (!Number.isFinite(parsedParentPid) || parsedParentPid <= 0) {
|
|
2389
|
-
throw new Error("register_agent requires valid parentPid");
|
|
2390
|
-
}
|
|
2391
|
-
const joinOptions = {
|
|
2392
|
-
parentPid: Number.isFinite(parsedParentPid) ? parsedParentPid : undefined,
|
|
2393
|
-
launchMode: launchMode || "",
|
|
2394
|
-
tmuxPane: tmuxPane || "",
|
|
2395
|
-
tty: tty || "",
|
|
2396
|
-
hostInjectSock: hostInjectSock || "",
|
|
2397
|
-
hostDaemonSock: hostDaemonSock || "",
|
|
2398
|
-
hostName: hostName || "",
|
|
2399
|
-
hostSessionId: hostSessionId || "",
|
|
2400
|
-
hostCapabilities: hostCapabilities && typeof hostCapabilities === "object"
|
|
2401
|
-
? hostCapabilities
|
|
2402
|
-
: null,
|
|
2403
|
-
reuseSessionId,
|
|
2404
|
-
reuseProviderSessionId,
|
|
2405
|
-
};
|
|
2406
|
-
if (skipSessionResolve) joinOptions.skipSessionResolve = true;
|
|
2407
|
-
|
|
2408
|
-
let finalNickname = nickname || "";
|
|
2409
|
-
let scopedNickname = applyProjectNicknamePrefix(projectRoot, finalNickname, {
|
|
2410
|
-
agentType: normalizeBusAgentType(agentType),
|
|
2301
|
+
const controlPlane = require("./controlPlaneService");
|
|
2302
|
+
const result = await controlPlane.registerAgentFull(projectRoot, {
|
|
2303
|
+
...req,
|
|
2304
|
+
agent_type: agentType,
|
|
2305
|
+
parentPid,
|
|
2306
|
+
}, {
|
|
2307
|
+
validateParentPid: true,
|
|
2308
|
+
checkNicknameConflicts: true,
|
|
2411
2309
|
});
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
tty: tty || "",
|
|
2415
|
-
agentType: normalizeBusAgentType(agentType),
|
|
2416
|
-
scopedNickname,
|
|
2417
|
-
});
|
|
2418
|
-
if (nickCheck.existing) {
|
|
2419
|
-
finalNickname = "";
|
|
2420
|
-
scopedNickname = "";
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
await eventBus.join(
|
|
2424
|
-
sessionId,
|
|
2425
|
-
normalizeBusAgentType(agentType),
|
|
2426
|
-
finalNickname,
|
|
2427
|
-
{ ...joinOptions, scopedNickname },
|
|
2428
|
-
);
|
|
2429
|
-
if (finalNickname) {
|
|
2430
|
-
eventBus.rename(subscriberId, finalNickname, "ufoo-agent", { scopedNickname });
|
|
2431
|
-
}
|
|
2432
|
-
eventBus.saveBusData();
|
|
2433
|
-
const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || finalNickname || "";
|
|
2310
|
+
const subscriberId = result.subscriber;
|
|
2311
|
+
const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || result.nickname || "";
|
|
2434
2312
|
|
|
2435
|
-
if (!skipSessionResolve && reuseProviderSessionId) {
|
|
2313
|
+
if (!skipSessionResolve && result.reuseProviderSessionId) {
|
|
2436
2314
|
if (providerSessions) {
|
|
2437
2315
|
providerSessions.set(subscriberId, {
|
|
2438
|
-
sessionId: reuseProviderSessionId,
|
|
2316
|
+
sessionId: result.reuseProviderSessionId,
|
|
2439
2317
|
source: "reuse",
|
|
2440
2318
|
updated_at: new Date().toISOString(),
|
|
2441
2319
|
});
|
|
@@ -2547,6 +2425,12 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2547
2425
|
tryResolveSession(1);
|
|
2548
2426
|
return;
|
|
2549
2427
|
}
|
|
2428
|
+
if (req.type === IPC_REQUEST_TYPES.REFRESH_STATUS) {
|
|
2429
|
+
cleanupInactiveSubscribers();
|
|
2430
|
+
const status = buildRuntimeStatus();
|
|
2431
|
+
ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2550
2434
|
};
|
|
2551
2435
|
|
|
2552
2436
|
ipcServer.listen(socketPath(projectRoot));
|
|
@@ -5,11 +5,9 @@ const net = require("net");
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { spawn } = require("child_process");
|
|
7
7
|
|
|
8
|
-
const EventBus = require("../../coordination/bus");
|
|
9
8
|
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
10
|
-
const { normalizeReportInput } = require("../../coordination/report/store");
|
|
11
|
-
const { enqueueAgentReport } = require("./reportControlBus");
|
|
12
9
|
const { isRunning, socketPath } = require("./index");
|
|
10
|
+
const controlPlane = require("./controlPlaneService");
|
|
13
11
|
const {
|
|
14
12
|
normalizeProjectRoot,
|
|
15
13
|
resolveGlobalControllerProjectRoot,
|
|
@@ -171,18 +169,6 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
|
|
|
171
169
|
},
|
|
172
170
|
]);
|
|
173
171
|
|
|
174
|
-
function normalizeBusAgentType(agentType = "") {
|
|
175
|
-
const value = String(agentType || "").trim().toLowerCase();
|
|
176
|
-
if (!value) return "mcp-agent";
|
|
177
|
-
if (value === "claude") return "claude-code";
|
|
178
|
-
if (value === "ucode" || value === "ufoo") return "ufoo-code";
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function nowIso() {
|
|
183
|
-
return new Date().toISOString();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
172
|
function cloneJson(value) {
|
|
187
173
|
return JSON.parse(JSON.stringify(value || {}));
|
|
188
174
|
}
|
|
@@ -286,10 +272,6 @@ async function suppressConsoleToStderr(fn) {
|
|
|
286
272
|
}
|
|
287
273
|
}
|
|
288
274
|
|
|
289
|
-
function createSessionId() {
|
|
290
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
275
|
function listRegisteredProjectRows() {
|
|
294
276
|
return listProjectRuntimes({ validate: true, cleanupTmp: true })
|
|
295
277
|
.filter((row) => !isGlobalControllerProjectRoot(row && row.project_root));
|
|
@@ -315,33 +297,6 @@ function resolveRegisteredProjectRoot(args = {}, options = {}) {
|
|
|
315
297
|
return match.project_root || normalized;
|
|
316
298
|
}
|
|
317
299
|
|
|
318
|
-
function ensureBusLoaded(projectRoot) {
|
|
319
|
-
const bus = new EventBus(projectRoot);
|
|
320
|
-
bus.ensureBus();
|
|
321
|
-
bus.loadBusData();
|
|
322
|
-
return bus;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function assertSubscriberExists(bus, subscriber) {
|
|
326
|
-
const meta = bus.subscriberManager.getSubscriber(subscriber);
|
|
327
|
-
if (!meta) {
|
|
328
|
-
const err = new Error(`subscriber not found: ${subscriber}`);
|
|
329
|
-
err.code = "subscriber_not_found";
|
|
330
|
-
throw err;
|
|
331
|
-
}
|
|
332
|
-
return meta;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function resolveSubscriberArg(args = {}) {
|
|
336
|
-
const subscriber = String(args.subscriber || args.source || "").trim();
|
|
337
|
-
if (!subscriber) {
|
|
338
|
-
const err = new Error("subscriber is required");
|
|
339
|
-
err.code = "invalid_subscriber";
|
|
340
|
-
throw err;
|
|
341
|
-
}
|
|
342
|
-
return subscriber;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
300
|
function connectSocket(sockPath, timeoutMs = 500) {
|
|
346
301
|
return new Promise((resolve, reject) => {
|
|
347
302
|
let timer = null;
|
|
@@ -438,165 +393,37 @@ async function handleMcpStatus(ctx = {}) {
|
|
|
438
393
|
|
|
439
394
|
async function handleRegisterAgent(ctx = {}, args = {}) {
|
|
440
395
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
441
|
-
|
|
442
|
-
const sessionId = String(args.session_id || args.sessionId || createSessionId()).trim();
|
|
443
|
-
const nickname = String(args.nickname || "").trim();
|
|
444
|
-
const launchMode = String(args.launch_mode || args.launchMode || "mcp").trim();
|
|
445
|
-
const capabilities = args.capabilities && typeof args.capabilities === "object"
|
|
446
|
-
? args.capabilities
|
|
447
|
-
: null;
|
|
448
|
-
const bus = ensureBusLoaded(projectRoot);
|
|
449
|
-
const result = await bus.subscriberManager.join(sessionId, agentType, nickname, {
|
|
450
|
-
parentPid: process.pid,
|
|
451
|
-
launchMode,
|
|
452
|
-
scopedNickname: String(args.scoped_nickname || args.scopedNickname || nickname || "").trim(),
|
|
453
|
-
hostName: "ufoo-mcp",
|
|
454
|
-
hostSessionId: `mcp-${process.pid}`,
|
|
455
|
-
hostCapabilities: capabilities,
|
|
456
|
-
});
|
|
457
|
-
const subscriber = result.subscriber;
|
|
458
|
-
const meta = bus.subscriberManager.getSubscriber(subscriber) || {};
|
|
459
|
-
meta.activity_state = String(args.activity_state || "ready");
|
|
460
|
-
meta.activity_since = nowIso();
|
|
461
|
-
meta.mcp_bridge = true;
|
|
462
|
-
if (capabilities) meta.mcp_capabilities = capabilities;
|
|
463
|
-
bus.saveBusData();
|
|
464
|
-
return {
|
|
465
|
-
ok: true,
|
|
466
|
-
project_root: projectRoot,
|
|
467
|
-
subscriber_id: subscriber,
|
|
468
|
-
subscriber,
|
|
469
|
-
session_id: sessionId,
|
|
470
|
-
agent_type: agentType,
|
|
471
|
-
nickname: meta.nickname || result.nickname || "",
|
|
472
|
-
scoped_nickname: meta.scoped_nickname || result.scopedNickname || "",
|
|
473
|
-
launch_mode: launchMode,
|
|
474
|
-
};
|
|
396
|
+
return controlPlane.registerAgent(projectRoot, args);
|
|
475
397
|
}
|
|
476
398
|
|
|
477
399
|
async function handleHeartbeatAgent(ctx = {}, args = {}) {
|
|
478
400
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
479
|
-
|
|
480
|
-
const bus = ensureBusLoaded(projectRoot);
|
|
481
|
-
const meta = assertSubscriberExists(bus, subscriber);
|
|
482
|
-
bus.subscriberManager.updateLastSeen(subscriber);
|
|
483
|
-
meta.status = "active";
|
|
484
|
-
bus.saveBusData();
|
|
485
|
-
return {
|
|
486
|
-
ok: true,
|
|
487
|
-
project_root: projectRoot,
|
|
488
|
-
subscriber,
|
|
489
|
-
last_seen: meta.last_seen,
|
|
490
|
-
};
|
|
401
|
+
return controlPlane.heartbeatAgent(projectRoot, args);
|
|
491
402
|
}
|
|
492
403
|
|
|
493
404
|
async function handlePublishActivityState(ctx = {}, args = {}) {
|
|
494
405
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
495
|
-
|
|
496
|
-
const activityState = String(args.activity_state || args.activityState || "").trim();
|
|
497
|
-
if (!activityState) {
|
|
498
|
-
const err = new Error("activity_state is required");
|
|
499
|
-
err.code = "invalid_activity_state";
|
|
500
|
-
throw err;
|
|
501
|
-
}
|
|
502
|
-
const bus = ensureBusLoaded(projectRoot);
|
|
503
|
-
const meta = assertSubscriberExists(bus, subscriber);
|
|
504
|
-
bus.subscriberManager.updateLastSeen(subscriber);
|
|
505
|
-
meta.status = "active";
|
|
506
|
-
meta.activity_state = activityState;
|
|
507
|
-
meta.activity_detail = String(args.detail || "").trim();
|
|
508
|
-
meta.activity_since = String(args.since || "").trim() || nowIso();
|
|
509
|
-
bus.saveBusData();
|
|
510
|
-
return {
|
|
511
|
-
ok: true,
|
|
512
|
-
project_root: projectRoot,
|
|
513
|
-
subscriber,
|
|
514
|
-
activity_state: meta.activity_state,
|
|
515
|
-
activity_detail: meta.activity_detail,
|
|
516
|
-
activity_since: meta.activity_since,
|
|
517
|
-
};
|
|
406
|
+
return controlPlane.publishActivityState(projectRoot, args);
|
|
518
407
|
}
|
|
519
408
|
|
|
520
409
|
async function handleUpdateAgentMetadata(ctx = {}, args = {}) {
|
|
521
410
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
522
|
-
|
|
523
|
-
const bus = ensureBusLoaded(projectRoot);
|
|
524
|
-
const meta = assertSubscriberExists(bus, subscriber);
|
|
525
|
-
const nickname = String(args.nickname || "").trim();
|
|
526
|
-
if (nickname) {
|
|
527
|
-
await bus.subscriberManager.rename(subscriber, nickname);
|
|
528
|
-
}
|
|
529
|
-
const metadata = args.metadata && typeof args.metadata === "object" ? args.metadata : {};
|
|
530
|
-
if (Object.keys(metadata).length > 0) {
|
|
531
|
-
meta.mcp_metadata = {
|
|
532
|
-
...(meta.mcp_metadata && typeof meta.mcp_metadata === "object" ? meta.mcp_metadata : {}),
|
|
533
|
-
...metadata,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
bus.subscriberManager.updateLastSeen(subscriber);
|
|
537
|
-
bus.saveBusData();
|
|
538
|
-
const nextMeta = bus.subscriberManager.getSubscriber(subscriber) || meta;
|
|
539
|
-
return {
|
|
540
|
-
ok: true,
|
|
541
|
-
project_root: projectRoot,
|
|
542
|
-
subscriber,
|
|
543
|
-
nickname: nextMeta.nickname || "",
|
|
544
|
-
scoped_nickname: nextMeta.scoped_nickname || nextMeta.nickname || "",
|
|
545
|
-
metadata: nextMeta.mcp_metadata || {},
|
|
546
|
-
};
|
|
411
|
+
return controlPlane.updateAgentMetadata(projectRoot, args);
|
|
547
412
|
}
|
|
548
413
|
|
|
549
414
|
async function handlePollInbox(ctx = {}, args = {}) {
|
|
550
415
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
551
|
-
|
|
552
|
-
const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0
|
|
553
|
-
? Math.floor(Number(args.limit))
|
|
554
|
-
: 50;
|
|
555
|
-
const bus = ensureBusLoaded(projectRoot);
|
|
556
|
-
assertSubscriberExists(bus, subscriber);
|
|
557
|
-
bus.subscriberManager.updateLastSeen(subscriber);
|
|
558
|
-
bus.saveBusData();
|
|
559
|
-
const pending = await bus.messageManager.check(subscriber);
|
|
560
|
-
return {
|
|
561
|
-
ok: true,
|
|
562
|
-
project_root: projectRoot,
|
|
563
|
-
subscriber,
|
|
564
|
-
count: pending.length,
|
|
565
|
-
messages: pending.slice(0, limit),
|
|
566
|
-
truncated: pending.length > limit,
|
|
567
|
-
};
|
|
416
|
+
return controlPlane.pollInbox(projectRoot, args);
|
|
568
417
|
}
|
|
569
418
|
|
|
570
419
|
async function handleReportAgentStatus(ctx = {}, args = {}) {
|
|
571
420
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
572
|
-
|
|
573
|
-
const report = normalizeReportInput({
|
|
574
|
-
...args,
|
|
575
|
-
agent_id: subscriber,
|
|
576
|
-
source: "mcp",
|
|
577
|
-
});
|
|
578
|
-
const queued = await enqueueAgentReport(projectRoot, report, { publisher: subscriber });
|
|
579
|
-
return {
|
|
580
|
-
ok: true,
|
|
581
|
-
project_root: projectRoot,
|
|
582
|
-
status: "queued",
|
|
583
|
-
request_id: queued.request_id,
|
|
584
|
-
report,
|
|
585
|
-
queued,
|
|
586
|
-
};
|
|
421
|
+
return controlPlane.reportAgentStatus(projectRoot, args);
|
|
587
422
|
}
|
|
588
423
|
|
|
589
424
|
async function handleUnregisterAgent(ctx = {}, args = {}) {
|
|
590
425
|
const projectRoot = resolveRegisteredProjectRoot(args, ctx);
|
|
591
|
-
|
|
592
|
-
const bus = ensureBusLoaded(projectRoot);
|
|
593
|
-
const ok = await bus.subscriberManager.leave(subscriber);
|
|
594
|
-
bus.saveBusData();
|
|
595
|
-
return {
|
|
596
|
-
ok,
|
|
597
|
-
project_root: projectRoot,
|
|
598
|
-
subscriber,
|
|
599
|
-
};
|
|
426
|
+
return controlPlane.unregisterAgent(projectRoot, args);
|
|
600
427
|
}
|
|
601
428
|
|
|
602
429
|
function findCustomTool(name) {
|
|
@@ -643,6 +470,7 @@ class UfooMcpServer {
|
|
|
643
470
|
};
|
|
644
471
|
this.initialized = false;
|
|
645
472
|
this.startup = null;
|
|
473
|
+
this.registeredSubscribers = [];
|
|
646
474
|
}
|
|
647
475
|
|
|
648
476
|
async ensureStarted() {
|
|
@@ -717,6 +545,17 @@ class UfooMcpServer {
|
|
|
717
545
|
...this.options,
|
|
718
546
|
toolCallId: id,
|
|
719
547
|
}));
|
|
548
|
+
if (name === "register_agent" && result && result.subscriber && result.project_root) {
|
|
549
|
+
this.registeredSubscribers.push({
|
|
550
|
+
subscriber: result.subscriber,
|
|
551
|
+
projectRoot: result.project_root,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (name === "unregister_agent" && result && result.subscriber) {
|
|
555
|
+
this.registeredSubscribers = this.registeredSubscribers.filter(
|
|
556
|
+
(entry) => entry.subscriber !== result.subscriber
|
|
557
|
+
);
|
|
558
|
+
}
|
|
720
559
|
return createJsonRpcResult(id, createMcpContent(result));
|
|
721
560
|
}
|
|
722
561
|
|
|
@@ -729,6 +568,17 @@ class UfooMcpServer {
|
|
|
729
568
|
return createJsonRpcError(id, MCP_ERROR_CODES.INTERNAL_ERROR, err.message || String(err), data);
|
|
730
569
|
}
|
|
731
570
|
}
|
|
571
|
+
|
|
572
|
+
cleanup() {
|
|
573
|
+
for (const { subscriber, projectRoot } of this.registeredSubscribers) {
|
|
574
|
+
try {
|
|
575
|
+
controlPlane.unregisterAgent(projectRoot, { subscriber });
|
|
576
|
+
} catch {
|
|
577
|
+
// best-effort cleanup
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
this.registeredSubscribers = [];
|
|
581
|
+
}
|
|
732
582
|
}
|
|
733
583
|
|
|
734
584
|
function createUfooMcpServer(options = {}) {
|
|
@@ -772,6 +622,9 @@ async function runMcpServer(options = {}) {
|
|
|
772
622
|
}
|
|
773
623
|
});
|
|
774
624
|
|
|
625
|
+
input.on("end", () => server.cleanup());
|
|
626
|
+
input.on("close", () => server.cleanup());
|
|
627
|
+
|
|
775
628
|
return server;
|
|
776
629
|
}
|
|
777
630
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
5
6
|
|
|
6
7
|
function asTrimmedString(value) {
|
|
7
8
|
if (typeof value !== "string") return "";
|
|
@@ -109,6 +110,49 @@ function resolveScopedNickname(projectRoot, meta = {}, fallback = "") {
|
|
|
109
110
|
return applyProjectNicknamePrefix(projectRoot, fallbackValue, { force: true });
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function checkAndCleanupNickname(projectRoot, nickname, { tty = "", agentType = "", scopedNickname = "" } = {}) {
|
|
114
|
+
const conflictNickname = scopedNickname || applyProjectNicknamePrefix(projectRoot, nickname, {
|
|
115
|
+
agentType,
|
|
116
|
+
force: true,
|
|
117
|
+
});
|
|
118
|
+
if (!conflictNickname) return { existing: null, cleaned: false };
|
|
119
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
120
|
+
try {
|
|
121
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
122
|
+
const entries = Object.entries(bus.agents || {})
|
|
123
|
+
.filter(([, meta]) => {
|
|
124
|
+
const candidate = resolveScopedNickname(projectRoot, meta);
|
|
125
|
+
return meta && candidate === conflictNickname;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (entries.length === 0) {
|
|
129
|
+
return { existing: null, cleaned: false };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const activeAgent = entries.find(([, meta]) => meta.status === "active");
|
|
133
|
+
if (activeAgent) {
|
|
134
|
+
const [existingId, existingMeta] = activeAgent;
|
|
135
|
+
const sameType = agentType && existingMeta.agent_type === agentType;
|
|
136
|
+
const isStub = sameType && !existingMeta.tty && !existingMeta.activity_state;
|
|
137
|
+
const sameTty = tty && existingMeta.tty === tty;
|
|
138
|
+
if (isStub || sameTty) {
|
|
139
|
+
delete bus.agents[existingId];
|
|
140
|
+
fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
|
|
141
|
+
return { existing: null, cleaned: true };
|
|
142
|
+
}
|
|
143
|
+
return { existing: existingId, cleaned: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const [agentId] of entries) {
|
|
147
|
+
delete bus.agents[agentId];
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(busPath, JSON.stringify(bus, null, 2));
|
|
150
|
+
return { existing: null, cleaned: true };
|
|
151
|
+
} catch {
|
|
152
|
+
return { existing: null, cleaned: false };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
112
156
|
module.exports = {
|
|
113
157
|
normalizeNicknameSegment,
|
|
114
158
|
buildProjectNicknamePrefix,
|
|
@@ -117,4 +161,5 @@ module.exports = {
|
|
|
117
161
|
stripProjectNicknamePrefix,
|
|
118
162
|
resolveDisplayNickname,
|
|
119
163
|
resolveScopedNickname,
|
|
164
|
+
checkAndCleanupNickname,
|
|
120
165
|
};
|