u-foo 1.5.0 → 1.7.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/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/modules/AGENTS.template.md +4 -102
- package/package.json +1 -1
- package/src/agent/activityDetector.js +328 -0
- package/src/agent/activityStatePublisher.js +67 -0
- package/src/agent/activityStateWriter.js +40 -0
- package/src/agent/internalRunner.js +13 -0
- package/src/agent/launcher.js +110 -7
- package/src/agent/notifier.js +73 -4
- package/src/agent/ptyRunner.js +81 -34
- package/src/agent/ufooAgent.js +192 -6
- package/src/bus/activate.js +22 -2
- package/src/bus/daemon.js +1 -1
- package/src/bus/inject.js +29 -10
- package/src/bus/message.js +1 -9
- package/src/bus/subscriber.js +34 -0
- package/src/bus/utils.js +10 -0
- package/src/chat/agentBar.js +21 -3
- package/src/chat/agentViewController.js +2 -0
- package/src/chat/commandExecutor.js +15 -0
- package/src/chat/daemonConnection.js +45 -7
- package/src/chat/daemonMessageRouter.js +22 -0
- package/src/chat/daemonTransport.js +13 -2
- package/src/chat/daemonTransportDefaults.js +1 -0
- package/src/chat/dashboardKeyController.js +9 -0
- package/src/chat/dashboardView.js +32 -9
- package/src/chat/index.js +176 -8
- package/src/chat/projectCloseController.js +119 -0
- package/src/chat/projectRuntimes.js +55 -0
- package/src/chat/statusLineController.js +52 -6
- package/src/chat/transport.js +41 -5
- package/src/cli.js +14 -0
- package/src/config.js +1 -0
- package/src/daemon/index.js +63 -5
- package/src/daemon/ipcServer.js +6 -1
- package/src/daemon/ops.js +189 -14
- package/src/daemon/status.js +17 -1
- package/src/init/index.js +32 -3
- package/src/terminal/adapterRouter.js +13 -1
- package/src/terminal/adapters/hostAdapter.js +409 -0
- package/src/ufoo/agentsStore.js +44 -0
package/src/init/index.js
CHANGED
|
@@ -180,23 +180,52 @@ class UfooInit {
|
|
|
180
180
|
|
|
181
181
|
let content = fs.readFileSync(filePath, "utf8");
|
|
182
182
|
const marker = "<!-- ufoo-template -->";
|
|
183
|
+
const block = `${marker}\n${template}\n${marker}`;
|
|
184
|
+
|
|
183
185
|
if (content.includes(marker)) {
|
|
186
|
+
// Replace existing marker block in-place
|
|
184
187
|
const startIdx = content.indexOf(marker);
|
|
185
188
|
const endIdx = content.indexOf(marker, startIdx + marker.length);
|
|
186
189
|
if (endIdx !== -1) {
|
|
187
190
|
content =
|
|
188
191
|
content.slice(0, startIdx) +
|
|
189
|
-
|
|
192
|
+
block +
|
|
190
193
|
content.slice(endIdx + marker.length);
|
|
191
194
|
} else {
|
|
192
|
-
content
|
|
195
|
+
content =
|
|
196
|
+
content.slice(0, startIdx) + block + content.slice(startIdx + marker.length);
|
|
193
197
|
}
|
|
194
198
|
} else {
|
|
195
|
-
|
|
199
|
+
// Insert after first heading line for visibility (not buried at end)
|
|
200
|
+
const headingEnd = this.findFirstHeadingEnd(content);
|
|
201
|
+
if (headingEnd !== -1) {
|
|
202
|
+
content =
|
|
203
|
+
content.slice(0, headingEnd) +
|
|
204
|
+
`\n${block}\n\n` +
|
|
205
|
+
content.slice(headingEnd);
|
|
206
|
+
} else {
|
|
207
|
+
content = `${block}\n\n${content}`;
|
|
208
|
+
}
|
|
196
209
|
}
|
|
197
210
|
fs.writeFileSync(filePath, content, "utf8");
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
findFirstHeadingEnd(content) {
|
|
214
|
+
// ATX heading: # ... (allow leading indentation and EOF without trailing newline)
|
|
215
|
+
const atxHeading = content.match(/^(?:[ \t]{0,3})#{1,6}[ \t]*[^\n]*(?:\n|$)/m);
|
|
216
|
+
// Setext heading: text line + underline (=== or ---)
|
|
217
|
+
const setextHeading = content.match(/^[^\n]+\n(?:=+|-+)[ \t]*(?:\n|$)/m);
|
|
218
|
+
|
|
219
|
+
let bestMatch = null;
|
|
220
|
+
if (atxHeading && setextHeading) {
|
|
221
|
+
bestMatch = atxHeading.index <= setextHeading.index ? atxHeading : setextHeading;
|
|
222
|
+
} else {
|
|
223
|
+
bestMatch = atxHeading || setextHeading;
|
|
224
|
+
}
|
|
225
|
+
if (!bestMatch) return -1;
|
|
226
|
+
return bestMatch.index + bestMatch[0].length;
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
/**
|
|
201
230
|
* 初始化 context 模块
|
|
202
231
|
*/
|
|
@@ -6,6 +6,7 @@ const { createTerminalAdapter } = require("./adapters/terminalAdapter");
|
|
|
6
6
|
const { createTmuxAdapter } = require("./adapters/tmuxAdapter");
|
|
7
7
|
const { createInternalQueueAdapter } = require("./adapters/internalQueueAdapter");
|
|
8
8
|
const { createInternalPtyAdapter } = require("./adapters/internalPtyAdapter");
|
|
9
|
+
const { createHostAdapter } = require("./adapters/hostAdapter");
|
|
9
10
|
|
|
10
11
|
function createTerminalAdapterRouter(options = {}) {
|
|
11
12
|
const {
|
|
@@ -35,7 +36,7 @@ function createTerminalAdapterRouter(options = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function getAdapter(params = {}) {
|
|
38
|
-
const { launchMode = "", agentId = "" } = params;
|
|
39
|
+
const { launchMode = "", agentId = "", meta = null } = params;
|
|
39
40
|
|
|
40
41
|
if (launchMode === "terminal") {
|
|
41
42
|
return createTerminalAdapter({
|
|
@@ -53,6 +54,17 @@ function createTerminalAdapterRouter(options = {}) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
if (launchMode === "host") {
|
|
58
|
+
return createHostAdapter({
|
|
59
|
+
createAdapter,
|
|
60
|
+
injectSock: meta?.host_inject_sock || meta?.hostInjectSock || "",
|
|
61
|
+
daemonSock: meta?.host_daemon_sock || meta?.hostDaemonSock || "",
|
|
62
|
+
hostName: meta?.host_name || meta?.hostName || "",
|
|
63
|
+
sessionId: meta?.host_session_id || meta?.hostSessionId || "",
|
|
64
|
+
hostCapabilities: meta?.host_capabilities || meta?.hostCapabilities || null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
if (launchMode === "internal-pty") {
|
|
57
69
|
return createInternalPtyAdapter({
|
|
58
70
|
sendRaw,
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
const { createTerminalCapabilities } = require("../adapterContract");
|
|
2
|
+
const net = require("net");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mapping from host protocol command names to ufoo capability flags.
|
|
6
|
+
* When a host reports supporting a command, the corresponding capability is enabled.
|
|
7
|
+
*/
|
|
8
|
+
const COMMAND_TO_CAPABILITY = {
|
|
9
|
+
activate: "supportsActivate",
|
|
10
|
+
snapshot: "supportsSnapshot",
|
|
11
|
+
subscribe: "supportsSubscribeFull",
|
|
12
|
+
subscribe_screen: "supportsSubscribeScreen",
|
|
13
|
+
close_session: "supportsWindowClose",
|
|
14
|
+
notify: "supportsNotifierInjector",
|
|
15
|
+
replay: "supportsReplay",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Send a JSON request to a Unix socket and return the parsed response.
|
|
20
|
+
* Expects the unified envelope: {v, request_id, ok, result|error}
|
|
21
|
+
*/
|
|
22
|
+
function sendToSocket(sockPath, request, options = {}) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
if (!sockPath) {
|
|
25
|
+
reject(new Error("socket path not set"));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
30
|
+
? options.timeoutMs
|
|
31
|
+
: 5000;
|
|
32
|
+
|
|
33
|
+
const client = net.createConnection(sockPath, () => {
|
|
34
|
+
client.write(JSON.stringify(request) + "\n");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let buffer = "";
|
|
38
|
+
const timeout = setTimeout(() => {
|
|
39
|
+
client.destroy();
|
|
40
|
+
reject(new Error("host socket timeout"));
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
|
|
43
|
+
client.on("data", (data) => {
|
|
44
|
+
buffer += data.toString("utf8");
|
|
45
|
+
const lines = buffer.split("\n");
|
|
46
|
+
buffer = lines.pop() || "";
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
try {
|
|
52
|
+
const res = JSON.parse(line);
|
|
53
|
+
client.end();
|
|
54
|
+
if (res.ok) {
|
|
55
|
+
resolve(res.result || {});
|
|
56
|
+
} else {
|
|
57
|
+
const err = new Error(res.error || "host request failed");
|
|
58
|
+
err.errorCode = res.error_code || "";
|
|
59
|
+
reject(err);
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
client.end();
|
|
63
|
+
reject(err);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
client.on("error", (err) => {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
reject(err);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
client.on("close", () => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getInjectSock() {
|
|
81
|
+
return process.env.UFOO_HOST_INJECT_SOCK
|
|
82
|
+
|| process.env.HORIZON_INJECT_SOCK // deprecated fallback
|
|
83
|
+
|| "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getDaemonSock() {
|
|
87
|
+
return process.env.UFOO_HOST_DAEMON_SOCK || "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeHostValue(value) {
|
|
91
|
+
return typeof value === "string" ? value.trim() : "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeHostCapabilities(hostCapabilities) {
|
|
95
|
+
if (!hostCapabilities || typeof hostCapabilities !== "object") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const normalized = { ...hostCapabilities };
|
|
99
|
+
const commands = Array.isArray(normalized.commands) ? normalized.commands : [];
|
|
100
|
+
const sessionCommands = Array.isArray(normalized.session_commands)
|
|
101
|
+
? normalized.session_commands
|
|
102
|
+
: [];
|
|
103
|
+
if (commands.length === 0 && sessionCommands.length > 0) {
|
|
104
|
+
normalized.commands = [...sessionCommands];
|
|
105
|
+
} else {
|
|
106
|
+
normalized.commands = [...commands];
|
|
107
|
+
}
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clearDynamicCapabilities(capabilities) {
|
|
112
|
+
for (const flag of Object.values(COMMAND_TO_CAPABILITY)) {
|
|
113
|
+
if (flag in capabilities) {
|
|
114
|
+
capabilities[flag] = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Map host-reported commands array to ufoo capability flags.
|
|
121
|
+
* Mutates the capabilities object in-place.
|
|
122
|
+
*/
|
|
123
|
+
function applyHostCapabilities(capabilities, hostCommands) {
|
|
124
|
+
if (!Array.isArray(hostCommands)) return;
|
|
125
|
+
for (const cmd of hostCommands) {
|
|
126
|
+
const flag = COMMAND_TO_CAPABILITY[cmd];
|
|
127
|
+
if (flag && flag in capabilities) {
|
|
128
|
+
capabilities[flag] = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function probeHostCapabilities(options = {}) {
|
|
134
|
+
const injectSock = normalizeHostValue(options.injectSock) || getInjectSock();
|
|
135
|
+
const daemonSock = normalizeHostValue(options.daemonSock) || getDaemonSock();
|
|
136
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
137
|
+
? options.timeoutMs
|
|
138
|
+
: 5000;
|
|
139
|
+
|
|
140
|
+
for (const sock of [injectSock, daemonSock]) {
|
|
141
|
+
if (!sock) continue;
|
|
142
|
+
try {
|
|
143
|
+
// eslint-disable-next-line no-await-in-loop
|
|
144
|
+
const result = await sendToSocket(sock, { type: "capabilities" }, { timeoutMs });
|
|
145
|
+
const normalized = normalizeHostCapabilities(result);
|
|
146
|
+
if (normalized) {
|
|
147
|
+
return normalized;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// try next socket
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* ufoo Terminal Host adapter — connects to any terminal host's per-session
|
|
159
|
+
* inject socket via the ufoo Terminal Host Protocol.
|
|
160
|
+
*
|
|
161
|
+
* Detects UFOO_HOST_SESSION_ID and UFOO_HOST_INJECT_SOCK env vars.
|
|
162
|
+
* Works with any host that implements the protocol (Horizon, Warp, etc.).
|
|
163
|
+
*
|
|
164
|
+
* After connect(), capabilities are dynamically updated based on what
|
|
165
|
+
* the host reports in its capabilities handshake response.
|
|
166
|
+
*/
|
|
167
|
+
function createHostAdapter(options = {}) {
|
|
168
|
+
const {
|
|
169
|
+
createAdapter = () => {},
|
|
170
|
+
injectSock = "",
|
|
171
|
+
daemonSock = "",
|
|
172
|
+
hostName = "",
|
|
173
|
+
sessionId = "",
|
|
174
|
+
hostCapabilities: initialHostCapabilities = null,
|
|
175
|
+
} = options;
|
|
176
|
+
|
|
177
|
+
// Start with base capabilities; additional flags are set after connect()
|
|
178
|
+
const capabilities = createTerminalCapabilities({
|
|
179
|
+
supportsSocketProtocol: true,
|
|
180
|
+
supportsSessionReuse: true,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const explicitInjectSock = normalizeHostValue(injectSock);
|
|
184
|
+
const explicitDaemonSock = normalizeHostValue(daemonSock);
|
|
185
|
+
const explicitHostName = normalizeHostValue(hostName);
|
|
186
|
+
const explicitSessionId = normalizeHostValue(sessionId);
|
|
187
|
+
const seededHostCapabilities = normalizeHostCapabilities(initialHostCapabilities);
|
|
188
|
+
|
|
189
|
+
// Cached host capabilities from handshake
|
|
190
|
+
let hostCapabilities = seededHostCapabilities ? { ...seededHostCapabilities } : null;
|
|
191
|
+
|
|
192
|
+
function resolveInjectSock() {
|
|
193
|
+
return explicitInjectSock || getInjectSock();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveDaemonSock() {
|
|
197
|
+
return explicitDaemonSock || getDaemonSock();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resetCapabilitiesToSeed() {
|
|
201
|
+
clearDynamicCapabilities(capabilities);
|
|
202
|
+
if (seededHostCapabilities) {
|
|
203
|
+
applyHostCapabilities(capabilities, seededHostCapabilities.commands);
|
|
204
|
+
hostCapabilities = { ...seededHostCapabilities };
|
|
205
|
+
} else {
|
|
206
|
+
hostCapabilities = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
resetCapabilitiesToSeed();
|
|
211
|
+
|
|
212
|
+
return createAdapter({
|
|
213
|
+
capabilities,
|
|
214
|
+
handlers: {
|
|
215
|
+
connect: async () => {
|
|
216
|
+
clearDynamicCapabilities(capabilities);
|
|
217
|
+
const result = await probeHostCapabilities({
|
|
218
|
+
injectSock: resolveInjectSock(),
|
|
219
|
+
daemonSock: resolveDaemonSock(),
|
|
220
|
+
});
|
|
221
|
+
if (result) {
|
|
222
|
+
hostCapabilities = result;
|
|
223
|
+
applyHostCapabilities(capabilities, result.commands);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
resetCapabilitiesToSeed();
|
|
227
|
+
return false;
|
|
228
|
+
},
|
|
229
|
+
disconnect: async () => {
|
|
230
|
+
clearDynamicCapabilities(capabilities);
|
|
231
|
+
hostCapabilities = null;
|
|
232
|
+
return true;
|
|
233
|
+
},
|
|
234
|
+
send: (data) => {
|
|
235
|
+
const sock = resolveInjectSock();
|
|
236
|
+
if (!sock) return false;
|
|
237
|
+
sendToSocket(sock, { type: "inject", command: String(data) }).catch(() => {});
|
|
238
|
+
return true;
|
|
239
|
+
},
|
|
240
|
+
sendRaw: (data) => {
|
|
241
|
+
const sock = resolveInjectSock();
|
|
242
|
+
if (!sock) return false;
|
|
243
|
+
sendToSocket(sock, { type: "raw", data: String(data) }).catch(() => {});
|
|
244
|
+
return true;
|
|
245
|
+
},
|
|
246
|
+
resize: (cols, rows) => {
|
|
247
|
+
const sock = resolveInjectSock();
|
|
248
|
+
if (!sock) return false;
|
|
249
|
+
sendToSocket(sock, { type: "resize", cols, rows }).catch(() => {});
|
|
250
|
+
return true;
|
|
251
|
+
},
|
|
252
|
+
snapshot: () => {
|
|
253
|
+
if (!capabilities.supportsSnapshot) return false;
|
|
254
|
+
const sock = resolveInjectSock();
|
|
255
|
+
if (!sock) return false;
|
|
256
|
+
sendToSocket(sock, { type: "snapshot" }).catch(() => {});
|
|
257
|
+
return true;
|
|
258
|
+
},
|
|
259
|
+
subscribe: () => {
|
|
260
|
+
if (!capabilities.supportsSubscribeFull) return false;
|
|
261
|
+
const sock = resolveInjectSock();
|
|
262
|
+
if (!sock) return false;
|
|
263
|
+
sendToSocket(sock, { type: "subscribe" }).catch(() => {});
|
|
264
|
+
return true;
|
|
265
|
+
},
|
|
266
|
+
activate: async () => {
|
|
267
|
+
if (!capabilities.supportsActivate) return false;
|
|
268
|
+
const sock = resolveInjectSock();
|
|
269
|
+
if (!sock) return false;
|
|
270
|
+
try {
|
|
271
|
+
await sendToSocket(sock, { type: "activate" });
|
|
272
|
+
return true;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
getState: () => ({
|
|
278
|
+
hostName: explicitHostName || process.env.UFOO_HOST_NAME || "",
|
|
279
|
+
sessionId: explicitSessionId || process.env.UFOO_HOST_SESSION_ID || process.env.HORIZON_SESSION_ID || "",
|
|
280
|
+
injectSock: resolveInjectSock(),
|
|
281
|
+
daemonSock: resolveDaemonSock(),
|
|
282
|
+
hostCapabilities,
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- Host command helpers (for callers that need async results) ---
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Request a terminal snapshot from the host.
|
|
292
|
+
* @param {string} [sockPath] - Override socket path
|
|
293
|
+
* @returns {Promise<{lines: string[], cols: number, rows: number, cursor?: {x: number, y: number}}>}
|
|
294
|
+
*/
|
|
295
|
+
async function requestSnapshot(sockPath) {
|
|
296
|
+
const sock = normalizeHostValue(sockPath) || getInjectSock();
|
|
297
|
+
return sendToSocket(sock, { type: "snapshot" });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Activate (focus) the host terminal window/tab.
|
|
302
|
+
* @param {string} [sockPath] - Override socket path
|
|
303
|
+
* @returns {Promise<{}>}
|
|
304
|
+
*/
|
|
305
|
+
async function requestActivate(sockPath) {
|
|
306
|
+
const sock = normalizeHostValue(sockPath) || getInjectSock();
|
|
307
|
+
return sendToSocket(sock, { type: "activate" });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Send a notification via the host.
|
|
312
|
+
* @param {string} message - Notification message
|
|
313
|
+
* @param {object} [opts] - Options: { title, urgency }
|
|
314
|
+
* @param {string} [sockPath] - Override socket path
|
|
315
|
+
* @returns {Promise<{}>}
|
|
316
|
+
*/
|
|
317
|
+
async function requestNotify(message, opts = {}, sockPath) {
|
|
318
|
+
const sock = normalizeHostValue(sockPath) || getInjectSock();
|
|
319
|
+
return sendToSocket(sock, { type: "notify", message, ...opts });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Close a terminal session via the inject socket.
|
|
324
|
+
* @param {string} [sockPath] - Override socket path
|
|
325
|
+
* @returns {Promise<{}>}
|
|
326
|
+
*/
|
|
327
|
+
async function requestCloseSession(sockPath) {
|
|
328
|
+
const sock = normalizeHostValue(sockPath) || getInjectSock();
|
|
329
|
+
return sendToSocket(sock, { type: "close_session" });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// --- Daemon lifecycle management (per-host, not per-session) ---
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create a new terminal session via the daemon management socket.
|
|
336
|
+
* @param {string} [daemonSock] - Override daemon socket path (defaults to env)
|
|
337
|
+
* @param {object} [opts] - Options: { group_id, source_session_id }
|
|
338
|
+
* @returns {Promise<{session_id: string, inject_sock: string}>}
|
|
339
|
+
*/
|
|
340
|
+
async function createSession(daemonSock, opts = {}) {
|
|
341
|
+
const sock = normalizeHostValue(daemonSock) || getDaemonSock();
|
|
342
|
+
const req = { type: "create_session" };
|
|
343
|
+
if (opts.group_id) req.group_id = opts.group_id;
|
|
344
|
+
if (opts.source_session_id) req.source_session_id = opts.source_session_id;
|
|
345
|
+
return sendToSocket(sock, req);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* List all terminal sessions via the daemon management socket.
|
|
350
|
+
* @param {string} [daemonSock] - Override daemon socket path
|
|
351
|
+
* @returns {Promise<{sessions: Array<{session_id: string, inject_sock: string}>}>}
|
|
352
|
+
*/
|
|
353
|
+
async function listSessions(daemonSock) {
|
|
354
|
+
const sock = normalizeHostValue(daemonSock) || getDaemonSock();
|
|
355
|
+
return sendToSocket(sock, { type: "list_sessions" });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Close a terminal session via the daemon management socket.
|
|
360
|
+
* @param {string} sessionId - Session to close
|
|
361
|
+
* @param {string} [daemonSock] - Override daemon socket path
|
|
362
|
+
* @returns {Promise<{}>}
|
|
363
|
+
*/
|
|
364
|
+
async function closeSession(sessionId, daemonSock) {
|
|
365
|
+
const sock = normalizeHostValue(daemonSock) || getDaemonSock();
|
|
366
|
+
return sendToSocket(sock, { type: "close_session", session_id: sessionId });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Query host capabilities via the daemon management socket.
|
|
371
|
+
* @param {string} [daemonSock] - Override daemon socket path
|
|
372
|
+
* @returns {Promise<object>}
|
|
373
|
+
*/
|
|
374
|
+
async function queryCapabilities(daemonSock) {
|
|
375
|
+
const sock = normalizeHostValue(daemonSock) || getDaemonSock();
|
|
376
|
+
return sendToSocket(sock, { type: "capabilities" });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Ping the daemon management socket.
|
|
381
|
+
* @param {string} [daemonSock] - Override daemon socket path
|
|
382
|
+
* @returns {Promise<{pong: true}>}
|
|
383
|
+
*/
|
|
384
|
+
async function pingDaemon(daemonSock) {
|
|
385
|
+
const sock = normalizeHostValue(daemonSock) || getDaemonSock();
|
|
386
|
+
return sendToSocket(sock, { type: "ping" });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
createHostAdapter,
|
|
391
|
+
// Host command helpers (async, for direct use)
|
|
392
|
+
requestSnapshot,
|
|
393
|
+
requestActivate,
|
|
394
|
+
requestNotify,
|
|
395
|
+
requestCloseSession,
|
|
396
|
+
// Daemon lifecycle API
|
|
397
|
+
createSession,
|
|
398
|
+
listSessions,
|
|
399
|
+
closeSession,
|
|
400
|
+
queryCapabilities,
|
|
401
|
+
pingDaemon,
|
|
402
|
+
// For testing
|
|
403
|
+
sendToSocket,
|
|
404
|
+
COMMAND_TO_CAPABILITY,
|
|
405
|
+
applyHostCapabilities,
|
|
406
|
+
clearDynamicCapabilities,
|
|
407
|
+
normalizeHostCapabilities,
|
|
408
|
+
probeHostCapabilities,
|
|
409
|
+
};
|
package/src/ufoo/agentsStore.js
CHANGED
|
@@ -94,8 +94,52 @@ function loadAgentsData(filePath) {
|
|
|
94
94
|
return normalizeAgentsData(data);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function parseTimestampMs(value) {
|
|
98
|
+
const ms = Date.parse(String(value || ""));
|
|
99
|
+
return Number.isFinite(ms) ? ms : Number.NaN;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeExternalActivityFields(targetMeta, diskMeta) {
|
|
103
|
+
if (!targetMeta || !diskMeta) return;
|
|
104
|
+
|
|
105
|
+
const diskState = toSafeString(diskMeta.activity_state);
|
|
106
|
+
if (!diskState) return;
|
|
107
|
+
|
|
108
|
+
const memoryState = toSafeString(targetMeta.activity_state);
|
|
109
|
+
const diskSince = toSafeString(diskMeta.activity_since);
|
|
110
|
+
const memorySince = toSafeString(targetMeta.activity_since);
|
|
111
|
+
const diskSinceMs = parseTimestampMs(diskSince);
|
|
112
|
+
const memorySinceMs = parseTimestampMs(memorySince);
|
|
113
|
+
|
|
114
|
+
if (!memoryState) {
|
|
115
|
+
targetMeta.activity_state = diskState;
|
|
116
|
+
if (diskSince) targetMeta.activity_since = diskSince;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const preferDisk = Number.isFinite(diskSinceMs)
|
|
121
|
+
&& (!Number.isFinite(memorySinceMs) || diskSinceMs > memorySinceMs);
|
|
122
|
+
if (preferDisk) {
|
|
123
|
+
targetMeta.activity_state = diskState;
|
|
124
|
+
targetMeta.activity_since = diskSince;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
function saveAgentsData(filePath, data) {
|
|
98
129
|
const normalized = normalizeAgentsData(data);
|
|
130
|
+
|
|
131
|
+
// Merge externally-managed fields from disk to avoid daemon in-memory writes
|
|
132
|
+
// overwriting fresher runner/notifier state updates.
|
|
133
|
+
const disk = readJSON(filePath, null);
|
|
134
|
+
if (disk && disk.agents && normalized.agents) {
|
|
135
|
+
for (const [id, diskMeta] of Object.entries(disk.agents)) {
|
|
136
|
+
if (!diskMeta || typeof diskMeta !== "object") continue;
|
|
137
|
+
const targetMeta = normalized.agents[id];
|
|
138
|
+
if (!targetMeta || typeof targetMeta !== "object") continue;
|
|
139
|
+
mergeExternalActivityFields(targetMeta, diskMeta);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
writeJSON(filePath, normalized);
|
|
100
144
|
}
|
|
101
145
|
|