u-foo 1.6.0 → 1.7.1

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -12,6 +12,7 @@ const { ActivityDetector } = require("./activityDetector");
12
12
  const { createActivityStatePublisher } = require("./activityStatePublisher");
13
13
  const { getUfooPaths } = require("../ufoo/paths");
14
14
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
15
+ const { probeHostCapabilities } = require("../terminal/adapters/hostAdapter");
15
16
  const PtyWrapper = require("./ptyWrapper");
16
17
  const ReadyDetector = require("./readyDetector");
17
18
 
@@ -152,6 +153,14 @@ function findPreviousSession(cwd, agentType, tty, tmuxPane) {
152
153
  function resolveLaunchMode() {
153
154
  const explicit = process.env.UFOO_LAUNCH_MODE || "";
154
155
  if (explicit) return explicit;
156
+ if (process.env.UFOO_HOST_SESSION_ID) return "host";
157
+ // Deprecated: HORIZON_SESSION_ID fallback (remove after migration)
158
+ if (process.env.HORIZON_SESSION_ID) {
159
+ if (process.env.UFOO_DEBUG) {
160
+ console.error("[launcher] HORIZON_SESSION_ID is deprecated, use UFOO_HOST_SESSION_ID");
161
+ }
162
+ return "host";
163
+ }
155
164
  if (process.env.TMUX_PANE) return "tmux";
156
165
  return "terminal";
157
166
  }
@@ -162,6 +171,48 @@ function shouldShowLaunchBanner(agentType = "") {
162
171
  return true;
163
172
  }
164
173
 
174
+ async function resolveHostRegistrationData(launchMode) {
175
+ if (launchMode !== "host") {
176
+ return {
177
+ hostInjectSock: "",
178
+ hostDaemonSock: "",
179
+ hostName: "",
180
+ hostSessionId: "",
181
+ hostCapabilities: null,
182
+ };
183
+ }
184
+
185
+ const hostInjectSock = process.env.UFOO_HOST_INJECT_SOCK
186
+ || process.env.HORIZON_INJECT_SOCK
187
+ || "";
188
+ const hostDaemonSock = process.env.UFOO_HOST_DAEMON_SOCK || "";
189
+ const hostName = process.env.UFOO_HOST_NAME || "";
190
+ const hostSessionId = process.env.UFOO_HOST_SESSION_ID
191
+ || process.env.HORIZON_SESSION_ID
192
+ || "";
193
+
194
+ let hostCapabilities = null;
195
+ if (hostInjectSock || hostDaemonSock) {
196
+ try {
197
+ hostCapabilities = await probeHostCapabilities({
198
+ injectSock: hostInjectSock,
199
+ daemonSock: hostDaemonSock,
200
+ timeoutMs: 1000,
201
+ });
202
+ } catch {
203
+ hostCapabilities = null;
204
+ }
205
+ }
206
+
207
+ return {
208
+ hostInjectSock,
209
+ hostDaemonSock,
210
+ hostName,
211
+ hostSessionId,
212
+ hostCapabilities,
213
+ };
214
+ }
215
+
165
216
  /**
166
217
  * Agent 启动器
167
218
  * 统一处理 agent 启动流程:初始化、daemon 注册、banner、命令执行
@@ -314,6 +365,13 @@ class AgentLauncher {
314
365
  const previousSession = shouldReuse
315
366
  ? findPreviousSession(this.cwd, this.agentType, tty, tmuxPane)
316
367
  : null;
368
+ const {
369
+ hostInjectSock,
370
+ hostDaemonSock,
371
+ hostName,
372
+ hostSessionId,
373
+ hostCapabilities,
374
+ } = await resolveHostRegistrationData(launchMode);
317
375
 
318
376
  const req = {
319
377
  type: IPC_REQUEST_TYPES.REGISTER_AGENT,
@@ -323,6 +381,11 @@ class AgentLauncher {
323
381
  launchMode,
324
382
  tmuxPane,
325
383
  tty,
384
+ hostInjectSock,
385
+ hostDaemonSock,
386
+ hostName,
387
+ hostSessionId,
388
+ hostCapabilities,
326
389
  skipProbe: process.env.UFOO_SKIP_SESSION_PROBE === "1",
327
390
  // 传递旧 session 信息用于复用(仅 terminal/tmux 模式)
328
391
  reuseSession: previousSession ? {
@@ -40,6 +40,11 @@ class AgentActivator {
40
40
  tty: meta.tty || "",
41
41
  tmux_pane: meta.tmux_pane || "",
42
42
  launch_mode: meta.launch_mode || "",
43
+ host_inject_sock: meta.host_inject_sock || "",
44
+ host_daemon_sock: meta.host_daemon_sock || "",
45
+ host_name: meta.host_name || "",
46
+ host_session_id: meta.host_session_id || "",
47
+ host_capabilities: meta.host_capabilities || null,
43
48
  };
44
49
  } catch (err) {
45
50
  throw new Error(`Failed to get agent info: ${err.message}`);
@@ -156,7 +161,19 @@ end tell`;
156
161
  activateTerminal,
157
162
  activateTmux,
158
163
  });
159
- const adapter = adapterRouter.getAdapter({ launchMode: info.launch_mode, agentId });
164
+ const adapter = adapterRouter.getAdapter({
165
+ launchMode: info.launch_mode,
166
+ agentId,
167
+ meta: info,
168
+ });
169
+
170
+ if (info.launch_mode === "host" && typeof adapter.connect === "function") {
171
+ try {
172
+ await adapter.connect();
173
+ } catch {
174
+ // fall back to seeded capabilities from bus metadata
175
+ }
176
+ }
160
177
 
161
178
  if (!adapter.capabilities.supportsActivate) {
162
179
  if (adapter.capabilities.supportsInternalQueueLoop) {
@@ -165,7 +182,10 @@ end tell`;
165
182
  throw new Error("Cannot activate: missing tty or tmux_pane for agent");
166
183
  }
167
184
 
168
- await adapter.activate();
185
+ const activated = await adapter.activate();
186
+ if (activated === false) {
187
+ throw new Error("Host activation request was rejected");
188
+ }
169
189
  }
170
190
  }
171
191
 
package/src/bus/daemon.js CHANGED
@@ -218,7 +218,7 @@ class BusDaemon {
218
218
  // - notifier/injector: terminal/tmux
219
219
  // - internal queue loop: internal/internal-pty
220
220
  // Bus daemon only handles legacy/unknown launch modes.
221
- const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber });
221
+ const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
222
222
  const { supportsNotifierInjector, supportsInternalQueueLoop } = adapter.capabilities;
223
223
  if (launchMode && (supportsNotifierInjector || supportsInternalQueueLoop)) {
224
224
  continue;
package/src/bus/inject.js CHANGED
@@ -184,18 +184,11 @@ class Injector {
184
184
  }
185
185
 
186
186
  /**
187
- * 使用 PTY socket 直接注入命令(无需macOS权限)
187
+ * 使用指定路径的 PTY socket 注入命令
188
188
  */
189
- async injectPty(subscriber, command) {
190
- const sockPath = this.getInjectSockPath(subscriber);
191
-
192
- if (!fs.existsSync(sockPath)) {
193
- throw new Error(`Inject socket not found: ${sockPath}`);
194
- }
195
-
189
+ async injectPtyAtPath(sockPath, command) {
196
190
  return new Promise((resolve, reject) => {
197
191
  const client = net.createConnection(sockPath, () => {
198
- // 发送inject请求
199
192
  client.write(JSON.stringify({ type: "inject", command }) + "\n");
200
193
  });
201
194
 
@@ -240,6 +233,19 @@ class Injector {
240
233
  });
241
234
  }
242
235
 
236
+ /**
237
+ * 使用 PTY socket 直接注入命令(无需macOS权限)
238
+ */
239
+ async injectPty(subscriber, command) {
240
+ const sockPath = this.getInjectSockPath(subscriber);
241
+
242
+ if (!fs.existsSync(sockPath)) {
243
+ throw new Error(`Inject socket not found: ${sockPath}`);
244
+ }
245
+
246
+ return this.injectPtyAtPath(sockPath, command);
247
+ }
248
+
243
249
  /**
244
250
  * 注入命令到订阅者的终端
245
251
  *
@@ -260,10 +266,23 @@ class Injector {
260
266
  const meta = this.getAgentMeta(subscriber) || {};
261
267
  const launchMode = meta.launch_mode || "";
262
268
  const adapterRouter = createTerminalAdapterRouter();
263
- const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriber });
269
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
264
270
  const supportsSocket = adapter.capabilities.supportsSocketProtocol;
265
271
  const supportsNotifier = adapter.capabilities.supportsNotifierInjector;
266
272
 
273
+ // 0. Try Terminal Host inject socket (ufoo Terminal Host Protocol)
274
+ const hostSock = (meta.host_inject_sock || "").toString();
275
+ if (hostSock && fs.existsSync(hostSock)) {
276
+ try {
277
+ logInject(`[inject] Using host inject socket: ${hostSock}`);
278
+ await this.injectPtyAtPath(hostSock, command);
279
+ logInject("[inject] Host inject success");
280
+ return;
281
+ } catch (err) {
282
+ logInject(`[inject] Host inject failed: ${err.message}, trying PTY socket`);
283
+ }
284
+ }
285
+
267
286
  // 1. 优先尝试 PTY socket(无需任何macOS权限)
268
287
  const injectSockPath = this.getInjectSockPath(subscriber);
269
288
  if (fs.existsSync(injectSockPath)) {
@@ -197,6 +197,22 @@ class SubscriberManager {
197
197
  const preservedTmuxPane = typeof existingMeta?.tmux_pane === "string" ? existingMeta.tmux_pane.trim() : "";
198
198
  const tmuxPane = explicitTmuxPane || envTmuxPane || preservedTmuxPane;
199
199
 
200
+ const hostInjectSock = typeof options.hostInjectSock === "string"
201
+ ? options.hostInjectSock.trim()
202
+ : "";
203
+ const hostDaemonSock = typeof options.hostDaemonSock === "string"
204
+ ? options.hostDaemonSock.trim()
205
+ : "";
206
+ const hostName = typeof options.hostName === "string"
207
+ ? options.hostName.trim()
208
+ : "";
209
+ const hostSessionId = typeof options.hostSessionId === "string"
210
+ ? options.hostSessionId.trim()
211
+ : "";
212
+ const hostCapabilities = options.hostCapabilities && typeof options.hostCapabilities === "object"
213
+ ? { ...options.hostCapabilities }
214
+ : null;
215
+
200
216
  this.busData.agents[subscriber] = {
201
217
  ...preserved,
202
218
  agent_type: agentType,
@@ -213,6 +229,22 @@ class SubscriberManager {
213
229
  launch_mode: launchMode,
214
230
  };
215
231
 
232
+ if (hostInjectSock) {
233
+ this.busData.agents[subscriber].host_inject_sock = hostInjectSock;
234
+ }
235
+ if (hostDaemonSock) {
236
+ this.busData.agents[subscriber].host_daemon_sock = hostDaemonSock;
237
+ }
238
+ if (hostName) {
239
+ this.busData.agents[subscriber].host_name = hostName;
240
+ }
241
+ if (hostSessionId) {
242
+ this.busData.agents[subscriber].host_session_id = hostSessionId;
243
+ }
244
+ if (hostCapabilities) {
245
+ this.busData.agents[subscriber].host_capabilities = hostCapabilities;
246
+ }
247
+
216
248
  const terminalApp = options.terminalApp || detectTerminalAppFromEnv();
217
249
  if (terminalApp) {
218
250
  this.busData.agents[subscriber].terminal_app = terminalApp;
@@ -29,6 +29,19 @@ function defaultResolveTerminalApp() {
29
29
  return "";
30
30
  }
31
31
 
32
+ function collectHostLaunchRequestContext(env = process.env) {
33
+ const hostInjectSock = String(env.UFOO_HOST_INJECT_SOCK || env.HORIZON_INJECT_SOCK || "").trim();
34
+ const hostDaemonSock = String(env.UFOO_HOST_DAEMON_SOCK || "").trim();
35
+ const hostName = String(env.UFOO_HOST_NAME || "").trim();
36
+ const hostSessionId = String(env.UFOO_HOST_SESSION_ID || env.HORIZON_SESSION_ID || "").trim();
37
+ const context = {};
38
+ if (hostInjectSock) context.host_inject_sock = hostInjectSock;
39
+ if (hostDaemonSock) context.host_daemon_sock = hostDaemonSock;
40
+ if (hostName) context.host_name = hostName;
41
+ if (hostSessionId) context.host_session_id = hostSessionId;
42
+ return context;
43
+ }
44
+
32
45
  async function withCapturedConsole(capture, fn) {
33
46
  const originalLog = console.log;
34
47
  const originalError = console.error;
@@ -445,6 +458,7 @@ function createCommandExecutor(options = {}) {
445
458
  count: Number.isFinite(count) ? count : 1,
446
459
  nickname,
447
460
  launch_scope: launchScope,
461
+ ...collectHostLaunchRequestContext(),
448
462
  };
449
463
  const terminalApp = String(resolveTerminalApp() || "").trim().toLowerCase();
450
464
  if (terminalApp === "terminal" || terminalApp === "iterm2") {
@@ -1126,4 +1140,5 @@ function createCommandExecutor(options = {}) {
1126
1140
 
1127
1141
  module.exports = {
1128
1142
  createCommandExecutor,
1143
+ collectHostLaunchRequestContext,
1129
1144
  };
package/src/chat/index.js CHANGED
@@ -44,7 +44,7 @@ const { createDaemonCoordinator } = require("./daemonCoordinator");
44
44
  const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
45
45
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
46
46
  const { createDaemonTransport } = require("./daemonTransport");
47
- const { listProjectRuntimes } = require("../projects/registry");
47
+ const { listProjectRuntimes, resolveRuntimeDir } = require("../projects/registry");
48
48
  const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
49
49
  const {
50
50
  sortProjectRuntimes,
@@ -725,7 +725,7 @@ async function runChat(projectRoot, options = {}) {
725
725
  if (!terminalAdapterRouter) return null;
726
726
  const meta = activeAgentMetaMap ? activeAgentMetaMap.get(agentId) : null;
727
727
  const agentLaunchMode = (meta && meta.launch_mode) || launchMode || "";
728
- return terminalAdapterRouter.getAdapter({ launchMode: agentLaunchMode, agentId });
728
+ return terminalAdapterRouter.getAdapter({ launchMode: agentLaunchMode, agentId, meta });
729
729
  }
730
730
 
731
731
  function getViewingAgentAdapter() {
@@ -1999,6 +1999,32 @@ async function runChat(projectRoot, options = {}) {
1999
1999
  requestStatus();
2000
2000
  }
2001
2001
  }, 5000);
2002
+
2003
+ // Global mode: watch runtime registry for new/removed projects
2004
+ if (globalMode) {
2005
+ const runtimeDir = resolveRuntimeDir();
2006
+ if (!fs.existsSync(runtimeDir)) {
2007
+ fs.mkdirSync(runtimeDir, { recursive: true });
2008
+ }
2009
+ let runtimeWatchDebounce = null;
2010
+ try {
2011
+ const watcher = fs.watch(runtimeDir, () => {
2012
+ if (runtimeWatchDebounce) return;
2013
+ runtimeWatchDebounce = setTimeout(() => {
2014
+ runtimeWatchDebounce = null;
2015
+ const prevCount = projectRuntimes.length;
2016
+ refreshProjectRuntimes();
2017
+ if (projectRuntimes.length !== prevCount) {
2018
+ renderDashboard();
2019
+ screen.render();
2020
+ }
2021
+ }, 300);
2022
+ });
2023
+ screen.on("destroy", () => watcher.close());
2024
+ } catch {
2025
+ // Fallback: ignore if fs.watch not supported
2026
+ }
2027
+ }
2002
2028
  screen.on("resize", () => {
2003
2029
  if (handleResizeInAgentView()) {
2004
2030
  return;
package/src/cli.js CHANGED
@@ -133,6 +133,19 @@ function requireOptional(name) {
133
133
  }
134
134
  }
135
135
 
136
+ function collectHostLaunchRequestContext(env = process.env) {
137
+ const hostInjectSock = String(env.UFOO_HOST_INJECT_SOCK || env.HORIZON_INJECT_SOCK || "").trim();
138
+ const hostDaemonSock = String(env.UFOO_HOST_DAEMON_SOCK || "").trim();
139
+ const hostName = String(env.UFOO_HOST_NAME || "").trim();
140
+ const hostSessionId = String(env.UFOO_HOST_SESSION_ID || env.HORIZON_SESSION_ID || "").trim();
141
+ const context = {};
142
+ if (hostInjectSock) context.host_inject_sock = hostInjectSock;
143
+ if (hostDaemonSock) context.host_daemon_sock = hostDaemonSock;
144
+ if (hostName) context.host_name = hostName;
145
+ if (hostSessionId) context.host_session_id = hostSessionId;
146
+ return context;
147
+ }
148
+
136
149
  function collectOption(value, previous) {
137
150
  const next = Array.isArray(previous) ? previous.slice() : [];
138
151
  const parts = String(value || "")
@@ -424,6 +437,7 @@ async function runCli(argv) {
424
437
  agent: normalizedAgent,
425
438
  nickname: nickname || "",
426
439
  count: 1,
440
+ ...collectHostLaunchRequestContext(),
427
441
  });
428
442
  const reply = resp?.data?.reply || `Launching ${normalizedAgent} agent...`;
429
443
  console.log(reply);
package/src/config.js CHANGED
@@ -27,6 +27,7 @@ function normalizeLaunchMode(value) {
27
27
  if (value === "internal") return "internal";
28
28
  if (value === "tmux") return "tmux";
29
29
  if (value === "terminal") return "terminal";
30
+ if (value === "host") return "host";
30
31
  return "auto";
31
32
  }
32
33
 
@@ -328,6 +328,16 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
328
328
  const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager, {
329
329
  launchScope: op.launch_scope || "",
330
330
  terminalApp: op.terminal_app || "",
331
+ hostInjectSock: op.host_inject_sock || op.hostInjectSock || "",
332
+ hostDaemonSock: op.host_daemon_sock || op.hostDaemonSock || "",
333
+ hostName: op.host_name || op.hostName || "",
334
+ hostSessionId: op.host_session_id || op.hostSessionId || "",
335
+ hostCapabilities:
336
+ (op.host_capabilities && typeof op.host_capabilities === "object")
337
+ ? op.host_capabilities
338
+ : ((op.hostCapabilities && typeof op.hostCapabilities === "object")
339
+ ? op.hostCapabilities
340
+ : null),
331
341
  });
332
342
  if (launchResult.mode === "internal" && launchResult.subscriberIds && launchResult.subscriberIds.length > 0) {
333
343
  const probeAgentType = agent === "codex"
@@ -980,7 +990,18 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
980
990
  }
981
991
  if (req.type === IPC_REQUEST_TYPES.LAUNCH_AGENT) {
982
992
  log(`launch_agent received: agent=${req.agent} count=${req.count}`);
983
- const { agent, count, nickname, launch_scope, terminal_app } = req;
993
+ const {
994
+ agent,
995
+ count,
996
+ nickname,
997
+ launch_scope,
998
+ terminal_app,
999
+ host_inject_sock,
1000
+ host_daemon_sock,
1001
+ host_name,
1002
+ host_session_id,
1003
+ host_capabilities,
1004
+ } = req;
984
1005
  const normalizedAgent = normalizeLaunchAgent(agent);
985
1006
  if (!normalizedAgent) {
986
1007
  socket.write(
@@ -1001,6 +1022,14 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1001
1022
  nickname: nickname || "",
1002
1023
  launch_scope: launch_scope || "",
1003
1024
  terminal_app: terminal_app || "",
1025
+ host_inject_sock: host_inject_sock || "",
1026
+ host_daemon_sock: host_daemon_sock || "",
1027
+ host_name: host_name || "",
1028
+ host_session_id: host_session_id || "",
1029
+ host_capabilities:
1030
+ host_capabilities && typeof host_capabilities === "object"
1031
+ ? host_capabilities
1032
+ : null,
1004
1033
  };
1005
1034
  try {
1006
1035
  const opsResults = await handleOps(projectRoot, [op], processManager);
@@ -1403,7 +1432,20 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1403
1432
  }
1404
1433
  if (req.type === IPC_REQUEST_TYPES.REGISTER_AGENT) {
1405
1434
  // Manual agent launch requests daemon to register it
1406
- const { agentType, nickname, parentPid, launchMode, tmuxPane, tty, skipProbe } = req;
1435
+ const {
1436
+ agentType,
1437
+ nickname,
1438
+ parentPid,
1439
+ launchMode,
1440
+ tmuxPane,
1441
+ tty,
1442
+ hostInjectSock,
1443
+ hostDaemonSock,
1444
+ hostName,
1445
+ hostSessionId,
1446
+ hostCapabilities,
1447
+ skipProbe,
1448
+ } = req;
1407
1449
  if (!agentType) {
1408
1450
  socket.write(
1409
1451
  `${JSON.stringify({
@@ -1451,6 +1493,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1451
1493
  launchMode: launchMode || "",
1452
1494
  tmuxPane: tmuxPane || "",
1453
1495
  tty: tty || "",
1496
+ hostInjectSock: hostInjectSock || "",
1497
+ hostDaemonSock: hostDaemonSock || "",
1498
+ hostName: hostName || "",
1499
+ hostSessionId: hostSessionId || "",
1500
+ hostCapabilities: hostCapabilities && typeof hostCapabilities === "object"
1501
+ ? hostCapabilities
1502
+ : null,
1454
1503
  reuseSessionId,
1455
1504
  reuseProviderSessionId,
1456
1505
  };
package/src/daemon/ops.js CHANGED
@@ -7,6 +7,9 @@ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
7
7
  const { isAgentPidAlive, getTtyProcessInfo } = require("../bus/utils");
8
8
  const { isITerm2 } = require("../terminal/detect");
9
9
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
10
+ const {
11
+ createSession: createHostSession,
12
+ } = require("../terminal/adapters/hostAdapter");
10
13
 
11
14
  function normalizeLaunchAgent(agent = "") {
12
15
  const value = String(agent || "").trim().toLowerCase();
@@ -75,6 +78,44 @@ function normalizeTerminalAppPreference(value = "") {
75
78
  return "";
76
79
  }
77
80
 
81
+ function normalizeOptionalString(value = "") {
82
+ return typeof value === "string" ? value.trim() : "";
83
+ }
84
+
85
+ function resolveHostLaunchContext(options = {}) {
86
+ return {
87
+ hostInjectSock:
88
+ normalizeOptionalString(options.hostInjectSock)
89
+ || normalizeOptionalString(process.env.UFOO_HOST_INJECT_SOCK)
90
+ || normalizeOptionalString(process.env.HORIZON_INJECT_SOCK),
91
+ hostDaemonSock:
92
+ normalizeOptionalString(options.hostDaemonSock)
93
+ || normalizeOptionalString(process.env.UFOO_HOST_DAEMON_SOCK),
94
+ hostName:
95
+ normalizeOptionalString(options.hostName)
96
+ || normalizeOptionalString(process.env.UFOO_HOST_NAME),
97
+ hostSessionId:
98
+ normalizeOptionalString(options.hostSessionId)
99
+ || normalizeOptionalString(process.env.UFOO_HOST_SESSION_ID)
100
+ || normalizeOptionalString(process.env.HORIZON_SESSION_ID),
101
+ hostCapabilities:
102
+ options.hostCapabilities && typeof options.hostCapabilities === "object"
103
+ ? { ...options.hostCapabilities }
104
+ : null,
105
+ };
106
+ }
107
+
108
+ function resolveConfiguredLaunchMode(configuredMode = "", options = {}) {
109
+ const mode = normalizeOptionalString(configuredMode);
110
+ if (mode === "internal" || mode === "tmux" || mode === "terminal" || mode === "host") {
111
+ return mode;
112
+ }
113
+ const hostContext = resolveHostLaunchContext(options);
114
+ if (hostContext.hostDaemonSock) return "host";
115
+ if (process.env.TMUX_PANE) return "tmux";
116
+ return "terminal";
117
+ }
118
+
78
119
  function resolveAgentId(projectRoot, agentId) {
79
120
  if (!agentId) return agentId;
80
121
  if (agentId.includes(":")) return agentId;
@@ -395,6 +436,64 @@ async function spawnManagedTerminalAgent(
395
436
  return { child: null, subscriberId: subscriberId || null };
396
437
  }
397
438
 
439
+ async function spawnManagedHostAgent(
440
+ projectRoot,
441
+ agent,
442
+ nickname = "",
443
+ processManager = null,
444
+ extraArgs = [],
445
+ extraEnv = "",
446
+ hostOptions = {}
447
+ ) {
448
+ void processManager;
449
+ const normalizedAgent = normalizeLaunchAgent(agent);
450
+ const binary = toTerminalBinary(normalizedAgent);
451
+ const agentType = toBusAgentType(normalizedAgent);
452
+ if (!binary || !agentType) {
453
+ throw new Error(`unsupported agent type: ${agent}`);
454
+ }
455
+
456
+ const hostContext = resolveHostLaunchContext(hostOptions);
457
+ if (!hostContext.hostDaemonSock) {
458
+ throw new Error("host launch requires UFOO_HOST_DAEMON_SOCK");
459
+ }
460
+
461
+ const existing = listSubscribers(projectRoot, agentType);
462
+ const createOptions = {};
463
+ if (hostOptions.groupId) {
464
+ createOptions.group_id = String(hostOptions.groupId).trim();
465
+ } else if (hostContext.hostSessionId) {
466
+ createOptions.source_session_id = hostContext.hostSessionId;
467
+ }
468
+
469
+ const args = Array.isArray(extraArgs) ? extraArgs : [];
470
+ const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
471
+ const envParts = ["UFOO_LAUNCH_MODE=host"];
472
+ if (nickname) {
473
+ envParts.push(`UFOO_NICKNAME=${shellEscape(nickname)}`);
474
+ }
475
+ if (extraEnv) {
476
+ envParts.push(String(extraEnv).trim());
477
+ }
478
+
479
+ const titleCmd = buildTitleCmd(nickname);
480
+ const launchCmd = `${envParts.join(" ")} ${binary}${argText}`.trim();
481
+ const runCmd = titleCmd
482
+ ? `cd ${shellEscape(projectRoot)} && ${titleCmd} && ${launchCmd}`
483
+ : `cd ${shellEscape(projectRoot)} && ${launchCmd}`;
484
+ createOptions.command = runCmd;
485
+
486
+ const created = await createHostSession(hostContext.hostDaemonSock, createOptions);
487
+ const sessionId = normalizeOptionalString(created?.session_id);
488
+ const injectSock = normalizeOptionalString(created?.inject_sock);
489
+ if (!sessionId || !injectSock) {
490
+ throw new Error("host create_session returned incomplete session info");
491
+ }
492
+
493
+ const subscriberId = await waitForNewSubscriber(projectRoot, agentType, existing, 20000);
494
+ return { child: null, subscriberId: subscriberId || null, sessionId, injectSock };
495
+ }
496
+
398
497
  async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null) {
399
498
  const runner = path.join(projectRoot, "bin", "ufoo.js");
400
499
  const logDir = getUfooPaths(projectRoot).runDir;
@@ -609,7 +708,7 @@ function spawnTmuxPane(projectRoot, agent, nickname = "", extraArgs = [], extraE
609
708
 
610
709
  async function launchAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, options = {}) {
611
710
  const config = loadConfig(projectRoot);
612
- const mode = config.launchMode || "terminal";
711
+ const mode = resolveConfiguredLaunchMode(config.launchMode, options);
613
712
  const launchScope = normalizeLaunchScope(options.launchScope, "inplace");
614
713
  const terminalApp = normalizeTerminalAppPreference(options.terminalApp);
615
714
  const normalizedAgent = normalizeLaunchAgent(agent);
@@ -665,6 +764,26 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
665
764
  }
666
765
  return { mode: "tmux", launchScope, subscriberIds: [] };
667
766
  }
767
+ if (mode === "host") {
768
+ const subscriberIds = [];
769
+ const hostContext = resolveHostLaunchContext(options);
770
+ for (let i = 0; i < count; i += 1) {
771
+ const defaultNick = normalizedAgent === "ufoo" ? "ucode" : normalizedAgent;
772
+ const nick = count > 1 ? `${nickname || defaultNick}-${i + 1}` : (nickname || "");
773
+ // eslint-disable-next-line no-await-in-loop
774
+ const result = await spawnManagedHostAgent(
775
+ projectRoot,
776
+ normalizedAgent,
777
+ nick,
778
+ processManager,
779
+ [],
780
+ "",
781
+ hostContext
782
+ );
783
+ if (result.subscriberId) subscriberIds.push(result.subscriberId);
784
+ }
785
+ return { mode: "host", launchScope, subscriberIds };
786
+ }
668
787
  // terminal mode - daemon 作为父进程,输出到终端窗口
669
788
  if (process.platform !== "darwin") {
670
789
  throw new Error("launchAgent with terminal mode is only supported on macOS Terminal.app");
@@ -834,12 +953,14 @@ async function closeAgent(projectRoot, agentId) {
834
953
  let tty = "";
835
954
  let terminalApp = "";
836
955
  let tmuxPane = "";
956
+ let meta = null;
837
957
  let found = false;
838
958
  try {
839
959
  const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
840
960
  const entry = bus.agents?.[resolvedId];
841
961
  if (entry) {
842
962
  found = true;
963
+ meta = entry;
843
964
  const parsedPid = Number.parseInt(entry.pid, 10);
844
965
  pid = Number.isFinite(parsedPid) && parsedPid > 0 ? parsedPid : 0;
845
966
  launchMode = entry.launch_mode || "";
@@ -856,7 +977,7 @@ async function closeAgent(projectRoot, agentId) {
856
977
  }
857
978
 
858
979
  const adapterRouter = createTerminalAdapterRouter();
859
- const adapter = adapterRouter.getAdapter({ launchMode, agentId: resolvedId });
980
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: resolvedId, meta });
860
981
  const canCloseWindow = process.platform === "darwin"
861
982
  && Boolean(adapter.capabilities.supportsWindowClose)
862
983
  && Boolean(tty);
@@ -149,7 +149,21 @@ function buildStatus(projectRoot, options = {}) {
149
149
  const tty = meta?.tty || "";
150
150
  const activity_state = meta?.activity_state || "";
151
151
  const activity_since = meta?.activity_since || "";
152
- return { id, nickname, display, launch_mode, tmux_pane, tty, activity_state, activity_since };
152
+ return {
153
+ id,
154
+ nickname,
155
+ display,
156
+ launch_mode,
157
+ tmux_pane,
158
+ tty,
159
+ activity_state,
160
+ activity_since,
161
+ host_inject_sock: meta?.host_inject_sock || "",
162
+ host_daemon_sock: meta?.host_daemon_sock || "",
163
+ host_name: meta?.host_name || "",
164
+ host_session_id: meta?.host_session_id || "",
165
+ host_capabilities: meta?.host_capabilities || null,
166
+ };
153
167
  });
154
168
 
155
169
  return {
@@ -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,410 @@
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, command }
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
+ if (opts.command) req.command = opts.command;
346
+ return sendToSocket(sock, req);
347
+ }
348
+
349
+ /**
350
+ * List all terminal sessions via the daemon management socket.
351
+ * @param {string} [daemonSock] - Override daemon socket path
352
+ * @returns {Promise<{sessions: Array<{session_id: string, inject_sock: string}>}>}
353
+ */
354
+ async function listSessions(daemonSock) {
355
+ const sock = normalizeHostValue(daemonSock) || getDaemonSock();
356
+ return sendToSocket(sock, { type: "list_sessions" });
357
+ }
358
+
359
+ /**
360
+ * Close a terminal session via the daemon management socket.
361
+ * @param {string} sessionId - Session to close
362
+ * @param {string} [daemonSock] - Override daemon socket path
363
+ * @returns {Promise<{}>}
364
+ */
365
+ async function closeSession(sessionId, daemonSock) {
366
+ const sock = normalizeHostValue(daemonSock) || getDaemonSock();
367
+ return sendToSocket(sock, { type: "close_session", session_id: sessionId });
368
+ }
369
+
370
+ /**
371
+ * Query host capabilities via the daemon management socket.
372
+ * @param {string} [daemonSock] - Override daemon socket path
373
+ * @returns {Promise<object>}
374
+ */
375
+ async function queryCapabilities(daemonSock) {
376
+ const sock = normalizeHostValue(daemonSock) || getDaemonSock();
377
+ return sendToSocket(sock, { type: "capabilities" });
378
+ }
379
+
380
+ /**
381
+ * Ping the daemon management socket.
382
+ * @param {string} [daemonSock] - Override daemon socket path
383
+ * @returns {Promise<{pong: true}>}
384
+ */
385
+ async function pingDaemon(daemonSock) {
386
+ const sock = normalizeHostValue(daemonSock) || getDaemonSock();
387
+ return sendToSocket(sock, { type: "ping" });
388
+ }
389
+
390
+ module.exports = {
391
+ createHostAdapter,
392
+ // Host command helpers (async, for direct use)
393
+ requestSnapshot,
394
+ requestActivate,
395
+ requestNotify,
396
+ requestCloseSession,
397
+ // Daemon lifecycle API
398
+ createSession,
399
+ listSessions,
400
+ closeSession,
401
+ queryCapabilities,
402
+ pingDaemon,
403
+ // For testing
404
+ sendToSocket,
405
+ COMMAND_TO_CAPABILITY,
406
+ applyHostCapabilities,
407
+ clearDynamicCapabilities,
408
+ normalizeHostCapabilities,
409
+ probeHostCapabilities,
410
+ };