u-foo 2.3.30 → 2.3.32

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.
Files changed (38) hide show
  1. package/package.json +5 -1
  2. package/scripts/chat-app-smoke.js +30 -0
  3. package/scripts/ink-demo.js +23 -0
  4. package/scripts/ink-smoke.js +30 -0
  5. package/scripts/ucode-app-smoke.js +36 -0
  6. package/src/chat/commandExecutor.js +6 -2
  7. package/src/chat/daemonMessageRouter.js +9 -1
  8. package/src/chat/daemonTransport.js +2 -1
  9. package/src/chat/dashboardKeyController.js +0 -40
  10. package/src/chat/dashboardView.js +0 -20
  11. package/src/chat/index.js +9 -1
  12. package/src/chat/inputSubmitHandler.js +34 -0
  13. package/src/chat/projectCloseController.js +1 -1
  14. package/src/chat/shellCommand.js +42 -0
  15. package/src/chat/transport.js +16 -3
  16. package/src/cli.js +4 -3
  17. package/src/code/agent.js +4 -0
  18. package/src/code/nativeRunner.js +74 -0
  19. package/src/code/taskDecomposer.js +5 -4
  20. package/src/code/tui.js +73 -561
  21. package/src/daemon/index.js +169 -27
  22. package/src/daemon/ipcServer.js +23 -1
  23. package/src/daemon/promptRequest.js +6 -1
  24. package/src/daemon/run.js +11 -4
  25. package/src/projects/runtimes.js +1 -1
  26. package/src/ufoo/agentRegistryDiagnostics.js +43 -0
  27. package/src/ui/MIGRATION.md +382 -0
  28. package/src/ui/components/ChatApp.js +2950 -0
  29. package/src/ui/components/DashboardBar.js +417 -0
  30. package/src/ui/components/InkDemo.js +96 -0
  31. package/src/ui/components/MultilineInput.js +387 -0
  32. package/src/ui/components/UcodeApp.js +813 -0
  33. package/src/ui/components/agentMirror.js +725 -0
  34. package/src/ui/components/chatReducer.js +337 -0
  35. package/src/ui/format/index.js +997 -0
  36. package/src/ui/index.js +9 -0
  37. package/src/ui/runInk.js +57 -0
  38. package/src/utils/nodeExecutable.js +26 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.30",
3
+ "version": "2.3.32",
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",
@@ -42,6 +42,8 @@
42
42
  "test": "jest",
43
43
  "test:watch": "jest --watch",
44
44
  "test:coverage": "jest --coverage",
45
+ "ink:demo": "node scripts/ink-demo.js",
46
+ "ink:smoke": "node scripts/ink-smoke.js",
45
47
  "bench:global-switch": "node scripts/global-chat-switch-benchmark.js"
46
48
  },
47
49
  "dependencies": {
@@ -51,7 +53,9 @@
51
53
  "chalk": "^4.1.2",
52
54
  "commander": "^13.1.0",
53
55
  "gray-matter": "^4.0.3",
56
+ "ink": "^5.2.1",
54
57
  "node-pty": "^1.1.0",
58
+ "react": "^18.3.1",
55
59
  "ws": "^8.19.0",
56
60
  "xterm-addon-serialize": "^0.11.0",
57
61
  "xterm-headless": "^5.3.0"
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Headless mount test for the ChatApp shell. Boots the ink TUI with stub
5
+ * props (no daemon, no real bootstrap) and checks the component tree
6
+ * renders without throwing. Used for CI parity with ucode-app-smoke.js.
7
+ */
8
+
9
+ const { runInk } = require("../src/ui/runInk");
10
+ const { createChatApp } = require("../src/ui/components/ChatApp");
11
+
12
+ (async () => {
13
+ const props = {
14
+ activeProjectRoot: process.cwd(),
15
+ globalMode: false,
16
+ globalScope: "project",
17
+ };
18
+ const handle = await runInk((React, ink) => {
19
+ const ChatApp = createChatApp({ React, ink, props, interactive: false });
20
+ return React.createElement(ChatApp);
21
+ }, { stdout: process.stdout, stderr: process.stderr });
22
+ await new Promise((r) => setTimeout(r, 80));
23
+ handle.unmount();
24
+ await handle.waitUntilExit().catch(() => undefined);
25
+ process.stdout.write("\nchat-app-smoke: ok\n");
26
+ process.exit(0);
27
+ })().catch((err) => {
28
+ process.stderr.write(`chat-app-smoke: failed: ${err && err.stack || err}\n`);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Interactive ink demo. Run from a real TTY:
6
+ * npm run ink:demo
7
+ * or
8
+ * node scripts/ink-demo.js
9
+ */
10
+
11
+ const { runInk } = require("../src/ui/runInk");
12
+ const { createInkDemo } = require("../src/ui/components/InkDemo");
13
+
14
+ (async () => {
15
+ const handle = await runInk((React, ink) => {
16
+ const InkDemo = createInkDemo({ React, ink, interactive: true });
17
+ return React.createElement(InkDemo);
18
+ });
19
+ await handle.waitUntilExit();
20
+ })().catch((err) => {
21
+ process.stderr.write(`ink-demo: ${err && err.stack || err}\n`);
22
+ process.exit(1);
23
+ });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Headless smoke test for the ink demo: mounts the component, lets it tick
5
+ * once, then unmounts. Exits with non-zero on any error. Used to confirm
6
+ * the CJS->ESM bridge and component tree work without occupying the TTY.
7
+ */
8
+
9
+ const { runInk } = require("../src/ui/runInk");
10
+ const { createInkDemo } = require("../src/ui/components/InkDemo");
11
+
12
+ (async () => {
13
+ const handle = await runInk((React, ink) => {
14
+ const InkDemo = createInkDemo({ React, ink, interactive: false });
15
+ return React.createElement(InkDemo);
16
+ }, {
17
+ stdout: process.stdout,
18
+ stderr: process.stderr,
19
+ stdin: process.stdin,
20
+ exitOnCtrlC: false,
21
+ });
22
+ await new Promise((r) => setTimeout(r, 100));
23
+ handle.unmount();
24
+ await handle.waitUntilExit().catch(() => undefined);
25
+ process.stdout.write("\nink-smoke: ok\n");
26
+ process.exit(0);
27
+ })().catch((err) => {
28
+ process.stderr.write(`ink-smoke: failed: ${err && err.stack || err}\n`);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Headless mount test for the UcodeApp shell. Boots the ink TUI with stub
5
+ * runner props, lets it render once, then unmounts. Used to confirm the ink
6
+ * code path stays compilable as P1 evolves.
7
+ */
8
+
9
+ const { runInk } = require("../src/ui/runInk");
10
+ const { createUcodeApp } = require("../src/ui/components/UcodeApp");
11
+
12
+ (async () => {
13
+ const props = {
14
+ stdin: process.stdin,
15
+ stdout: process.stdout,
16
+ runSingleCommand: () => ({ kind: "empty" }),
17
+ runNaturalLanguageTask: async () => ({ ok: true, summary: "ok" }),
18
+ runUbusCommand: async () => ({ ok: false, error: "ubus unsupported", summary: "" }),
19
+ formatNlResult: () => "ok",
20
+ workspaceRoot: process.cwd(),
21
+ state: { model: "test-model", sessionId: "smoke", engine: "ufoo-core" },
22
+ autoBus: { enabled: false, getPendingCount: () => 0, subscriberId: "" },
23
+ };
24
+ const handle = await runInk((React, ink) => {
25
+ const UcodeApp = createUcodeApp({ React, ink, props, interactive: false });
26
+ return React.createElement(UcodeApp);
27
+ }, { stdout: process.stdout, stderr: process.stderr });
28
+ await new Promise((r) => setTimeout(r, 80));
29
+ handle.unmount();
30
+ await handle.waitUntilExit().catch(() => undefined);
31
+ process.stdout.write("\nucode-app-smoke: ok\n");
32
+ process.exit(0);
33
+ })().catch((err) => {
34
+ process.stderr.write(`ucode-app-smoke: failed: ${err && err.stack || err}\n`);
35
+ process.exit(1);
36
+ });
@@ -237,7 +237,7 @@ function createCommandExecutor(options = {}) {
237
237
 
238
238
  if (subcommand === "stop") {
239
239
  statusMsg("{gray-fg}⚙{/gray-fg} Stopping daemon...");
240
- stopDaemon(targetRoot);
240
+ stopDaemon(targetRoot, { source: "chat-command:/daemon stop" });
241
241
  await sleep(1000);
242
242
  if (!isDaemonRunning(targetRoot)) {
243
243
  statusMsg("{gray-fg}✓{/gray-fg} Daemon stopped");
@@ -249,8 +249,12 @@ function createCommandExecutor(options = {}) {
249
249
 
250
250
  if (subcommand === "restart") {
251
251
  statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
252
- stopDaemon(targetRoot);
252
+ stopDaemon(targetRoot, { source: "chat-command:/daemon restart" });
253
253
  await sleep(500);
254
+ if (isDaemonRunning(targetRoot)) {
255
+ statusMsg("{gray-fg}✗{/gray-fg} Failed to stop daemon");
256
+ return;
257
+ }
254
258
  startDaemon(targetRoot);
255
259
  await sleep(1000);
256
260
  if (isDaemonRunning(targetRoot)) {
@@ -415,7 +415,15 @@ function createDaemonMessageRouter(options = {}) {
415
415
  const delta = typeof streamPayload.delta === "string"
416
416
  ? decodeEscapedNewlines(streamPayload.delta)
417
417
  : "";
418
- if (delta) writeToAgentTerm(delta);
418
+ if (delta || streamPayload.done) {
419
+ writeToAgentTerm(delta, {
420
+ data,
421
+ publisher,
422
+ streamPayload,
423
+ done: Boolean(streamPayload.done),
424
+ reason: streamPayload.reason || "",
425
+ });
426
+ }
419
427
  } else if (displayMessage) {
420
428
  writeToAgentTerm(`${displayMessage}\r\n`);
421
429
  }
@@ -29,6 +29,7 @@ function createDaemonTransport(options = {}) {
29
29
 
30
30
  async function connectClientForTarget(override = {}) {
31
31
  const target = resolveTarget(override);
32
+ const autoStart = override.autoStart !== false;
32
33
  let client = await connectWithRetry(
33
34
  target.sockPath,
34
35
  primaryRetries,
@@ -39,7 +40,7 @@ function createDaemonTransport(options = {}) {
39
40
  // Retry once with a fresh daemon start and longer wait.
40
41
  // Check if a restart is already in progress via the explicit restart flow.
41
42
  const isExplicitRestartInProgress = restartLocks.get(target.projectRoot);
42
- if (!isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
43
+ if (autoStart && !isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
43
44
  startDaemon(target.projectRoot);
44
45
  await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
45
46
  }
@@ -18,7 +18,6 @@ function createDashboardKeyController(options = {}) {
18
18
  exitDashboardMode = () => {},
19
19
  setLaunchMode = () => {},
20
20
  setAgentProvider = () => {},
21
- setAutoResume = () => {},
22
21
  clampAgentWindow = () => {},
23
22
  clampAgentWindowWithSelection = () => {},
24
23
  requestProjectSwitch = () => {},
@@ -314,44 +313,6 @@ function createDashboardKeyController(options = {}) {
314
313
  return true;
315
314
  }
316
315
 
317
- function handleResumeKey(key) {
318
- if (key.name === "left") {
319
- state.selectedResumeIndex = state.selectedResumeIndex <= 0
320
- ? state.resumeOptions.length - 1
321
- : state.selectedResumeIndex - 1;
322
- renderDashboardAndScreen();
323
- return true;
324
- }
325
-
326
- if (key.name === "right") {
327
- state.selectedResumeIndex = state.selectedResumeIndex >= state.resumeOptions.length - 1
328
- ? 0
329
- : state.selectedResumeIndex + 1;
330
- renderDashboardAndScreen();
331
- return true;
332
- }
333
-
334
- if (key.name === "up") {
335
- state.dashboardView = "provider";
336
- renderDashboardAndScreen();
337
- return true;
338
- }
339
-
340
- if (key.name === "enter" || key.name === "return") {
341
- const selected = state.resumeOptions[state.selectedResumeIndex];
342
- if (selected) setAutoResume(selected.value);
343
- exitDashboardMode(false);
344
- return true;
345
- }
346
-
347
- if (key.name === "escape") {
348
- exitDashboardMode(false);
349
- return true;
350
- }
351
-
352
- return true;
353
- }
354
-
355
316
  function handleProjectsKey(key) {
356
317
  const projects = Array.isArray(state.projects) ? state.projects : [];
357
318
  if (projects.length === 0) {
@@ -557,7 +518,6 @@ function createDashboardKeyController(options = {}) {
557
518
 
558
519
  if (state.dashboardView === "mode") return handleModeKey(key);
559
520
  if (state.dashboardView === "provider") return handleProviderKey(key);
560
- if (state.dashboardView === "resume") return handleResumeKey(key);
561
521
  if (state.dashboardView === "cron") return handleCronKey(key);
562
522
 
563
523
  return handleAgentsKey(key);
@@ -174,11 +174,9 @@ function buildDashboardDetailLine(options = {}) {
174
174
  getAgentState = () => "",
175
175
  selectedModeIndex = 0,
176
176
  selectedProviderIndex = 0,
177
- selectedResumeIndex = 0,
178
177
  selectedCronIndex = -1,
179
178
  cronTasks = [],
180
179
  providerOptions = [],
181
- resumeOptions = [],
182
180
  dashHints = {},
183
181
  modeOptions = DEFAULT_MODE_OPTIONS,
184
182
  } = options;
@@ -210,18 +208,6 @@ function buildDashboardDetailLine(options = {}) {
210
208
  return { content, windowStart };
211
209
  }
212
210
 
213
- if (dashboardView === "resume") {
214
- const resumeParts = resumeOptions.map((opt, i) => {
215
- if (i === selectedResumeIndex) {
216
- return `{inverse}${opt.label}{/inverse}`;
217
- }
218
- return `{cyan-fg}${opt.label}{/cyan-fg}`;
219
- });
220
- content += `{gray-fg}Resume:{/gray-fg} ${resumeParts.join(" ")}`;
221
- content += ` {gray-fg}│ ${dashHints.resume || ""}{/gray-fg}`;
222
- return { content, windowStart };
223
- }
224
-
225
211
  if (dashboardView === "cron") {
226
212
  const items = Array.isArray(cronTasks) ? cronTasks : [];
227
213
  const summary = items.length > 0
@@ -295,13 +281,11 @@ function computeDashboardContent(options = {}) {
295
281
  agentProvider = "codex-cli",
296
282
  selectedModeIndex = 0,
297
283
  selectedProviderIndex = 0,
298
- selectedResumeIndex = 0,
299
284
  selectedCronIndex = -1,
300
285
  cronTasks = [],
301
286
  loopSummary = null,
302
287
  pendingReports = 0,
303
288
  providerOptions = [],
304
- resumeOptions = [],
305
289
  dashHints = {},
306
290
  modeOptions = DEFAULT_MODE_OPTIONS,
307
291
  } = options;
@@ -357,11 +341,9 @@ function computeDashboardContent(options = {}) {
357
341
  getAgentState,
358
342
  selectedModeIndex,
359
343
  selectedProviderIndex,
360
- selectedResumeIndex,
361
344
  selectedCronIndex,
362
345
  cronTasks,
363
346
  providerOptions,
364
- resumeOptions,
365
347
  dashHints,
366
348
  modeOptions,
367
349
  });
@@ -383,11 +365,9 @@ function computeDashboardContent(options = {}) {
383
365
  getAgentState,
384
366
  selectedModeIndex,
385
367
  selectedProviderIndex,
386
- selectedResumeIndex,
387
368
  selectedCronIndex,
388
369
  cronTasks,
389
370
  providerOptions,
390
- resumeOptions,
391
371
  dashHints,
392
372
  modeOptions,
393
373
  });
package/src/chat/index.js CHANGED
@@ -69,7 +69,7 @@ const {
69
69
 
70
70
  const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
71
71
 
72
- async function runChat(projectRoot, options = {}) {
72
+ async function runChatBlessed(projectRoot, options = {}) {
73
73
  const globalMode = options && options.globalMode === true;
74
74
  const DASHBOARD_HEIGHT = globalMode ? 2 : 1;
75
75
  let activeProjectRoot = projectRoot;
@@ -2211,4 +2211,12 @@ async function runChat(projectRoot, options = {}) {
2211
2211
  screen.render();
2212
2212
  }
2213
2213
 
2214
+ async function runChat(projectRoot, options = {}) {
2215
+ if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
2216
+ return runChatBlessed(projectRoot, options);
2217
+ }
2218
+ const { runChatInk } = require("../ui/components/ChatApp");
2219
+ return runChatInk(projectRoot, options);
2220
+ }
2221
+
2214
2222
  module.exports = { runChat };
@@ -1,6 +1,7 @@
1
1
  const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
2
2
  const { decodeEscapedNewlines } = require("./text");
3
3
  const { shouldEchoCommandInChat } = require("./commands");
4
+ const { parseShellCommand, runShellCommand: defaultRunShellCommand } = require("./shellCommand");
4
5
 
5
6
  function createInputSubmitHandler(options = {}) {
6
7
  const {
@@ -24,6 +25,8 @@ function createInputSubmitHandler(options = {}) {
24
25
  commitInputHistory = () => {},
25
26
  focusInput = () => {},
26
27
  renderScreen = () => {}, // Add renderScreen callback
28
+ runShellCommand = defaultRunShellCommand,
29
+ getShellCwd = () => process.cwd(),
27
30
  } = options;
28
31
 
29
32
  if (!state || typeof state !== "object") {
@@ -90,6 +93,37 @@ function createInputSubmitHandler(options = {}) {
90
93
 
91
94
  commitInputHistory(text);
92
95
 
96
+ const shellCommand = parseShellCommand(text);
97
+ if (shellCommand) {
98
+ logMessage("user", `{gray-fg}!{/gray-fg} ${escapeBlessed(shellCommand)}`);
99
+ queueStatusLine(`Running: ${escapeBlessed(shellCommand)}`);
100
+ renderScreen();
101
+ try {
102
+ const result = await runShellCommand(shellCommand, { cwd: getShellCwd() });
103
+ const stdout = String(result && result.stdout ? result.stdout : "").trimEnd();
104
+ const stderr = String(result && result.stderr ? result.stderr : "").trimEnd();
105
+ if (stdout) {
106
+ stdout.split(/\r?\n/).forEach((line) => logMessage("system", escapeBlessed(line)));
107
+ }
108
+ if (stderr) {
109
+ stderr.split(/\r?\n/).forEach((line) => logMessage(result && result.ok ? "system" : "error", escapeBlessed(line)));
110
+ }
111
+ if (!stdout && !stderr) {
112
+ logMessage("system", "{gray-fg}(no output){/gray-fg}");
113
+ }
114
+ if (result && result.ok) {
115
+ queueStatusLine(`Done: ${escapeBlessed(shellCommand)}`);
116
+ } else {
117
+ const suffix = result && result.signal ? ` signal ${result.signal}` : ` exit ${result && result.code != null ? result.code : 1}`;
118
+ logMessage("error", `{white-fg}✗{/white-fg} Command failed:${escapeBlessed(suffix)}`);
119
+ }
120
+ } catch (err) {
121
+ logMessage("error", `{white-fg}✗{/white-fg} Command error: ${escapeBlessed(err && err.message ? err.message : err)}`);
122
+ }
123
+ focusInput();
124
+ return;
125
+ }
126
+
93
127
  if (state.targetAgent) {
94
128
  const label = getAgentLabel(state.targetAgent);
95
129
  logMessage(
@@ -86,7 +86,7 @@ function createProjectCloseController(options = {}) {
86
86
  }
87
87
 
88
88
  const wasRunning = Boolean(isRunning(projectRoot));
89
- stopDaemon(projectRoot);
89
+ stopDaemon(projectRoot, { source: `project-close:${projectRoot}` });
90
90
 
91
91
  refreshProjects();
92
92
  renderDashboard();
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ const { exec } = require("child_process");
4
+
5
+ function parseShellCommand(text) {
6
+ const value = String(text || "").trim();
7
+ if (!value.startsWith("!")) return null;
8
+ const command = value.slice(1).trim();
9
+ if (!command) return null;
10
+ return command;
11
+ }
12
+
13
+ function runShellCommand(command, options = {}) {
14
+ const cmd = String(command || "").trim();
15
+ if (!cmd) return Promise.resolve({ ok: false, code: null, stdout: "", stderr: "empty command" });
16
+ const cwd = options.cwd || process.cwd();
17
+ const timeout = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 120000;
18
+ const maxBuffer = Number.isFinite(options.maxBuffer) ? options.maxBuffer : 1024 * 1024;
19
+ return new Promise((resolve) => {
20
+ exec(cmd, {
21
+ cwd,
22
+ env: process.env,
23
+ timeout,
24
+ maxBuffer,
25
+ shell: process.env.SHELL || "/bin/sh",
26
+ }, (error, stdout, stderr) => {
27
+ resolve({
28
+ ok: !error,
29
+ code: error && Number.isFinite(error.code) ? error.code : 0,
30
+ signal: error && error.signal ? error.signal : null,
31
+ stdout: String(stdout || ""),
32
+ stderr: String(stderr || ""),
33
+ error: error && error.message ? error.message : "",
34
+ });
35
+ });
36
+ });
37
+ }
38
+
39
+ module.exports = {
40
+ parseShellCommand,
41
+ runShellCommand,
42
+ };
@@ -2,6 +2,7 @@ const net = require("net");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
  const { spawn, spawnSync } = require("child_process");
5
+ const { resolveNodeExecutable } = require("../utils/nodeExecutable");
5
6
 
6
7
  function connectSocket(sockPath, options = {}) {
7
8
  const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
@@ -57,21 +58,33 @@ function startDaemon(projectRoot, options = {}) {
57
58
  const env = options.forceResume
58
59
  ? { ...process.env, UFOO_FORCE_RESUME: "1" }
59
60
  : process.env;
60
- const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
61
+ const child = spawn(resolveNodeExecutable(), [daemonBin, "daemon", "--start"], {
61
62
  detached: true,
62
63
  stdio: "ignore",
63
64
  cwd: projectRoot,
64
65
  env,
65
66
  });
67
+ child.on("error", (err) => {
68
+ if (typeof options.onError === "function") {
69
+ options.onError(err);
70
+ }
71
+ });
66
72
  child.unref();
73
+ return child;
67
74
  }
68
75
 
69
- function stopDaemon(projectRoot) {
76
+ function stopDaemon(projectRoot, options = {}) {
70
77
  const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
71
- spawnSync(process.execPath, [daemonBin, "daemon", "--stop"], {
78
+ const source = String(
79
+ options.source
80
+ || `chat-transport pid=${process.pid} cwd=${process.cwd()} argv=${process.argv.join(" ")}`
81
+ );
82
+ const result = spawnSync(resolveNodeExecutable(), [daemonBin, "daemon", "--stop"], {
72
83
  stdio: "ignore",
73
84
  cwd: projectRoot,
85
+ env: { ...process.env, UFOO_DAEMON_STOP_SOURCE: source },
74
86
  });
87
+ return Boolean(result && !result.error && result.status === 0);
75
88
  }
76
89
 
77
90
  async function connectWithRetry(sockPath, retries, delayMs, options = {}) {
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ const { resolveSoloAgentType } = require("./solo/commands");
14
14
  const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
15
15
  const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
16
16
  const { getUfooPaths } = require("./ufoo/paths");
17
+ const { resolveNodeExecutable } = require("./utils/nodeExecutable");
17
18
 
18
19
  function getPackageRoot() {
19
20
  return path.resolve(__dirname, "..");
@@ -60,7 +61,7 @@ async function connectWithRetry(sockPath, retries, delayMs) {
60
61
  async function ensureDaemonRunning(projectRoot) {
61
62
  if (isRunning(projectRoot)) return;
62
63
  const repoRoot = getPackageRoot();
63
- run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "daemon", "start"]);
64
+ run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), "daemon", "start"]);
64
65
  const sock = socketPath(projectRoot);
65
66
  for (let i = 0; i < 30; i += 1) {
66
67
  if (fs.existsSync(sock)) {
@@ -1733,7 +1734,7 @@ async function runCli(argv) {
1733
1734
  return;
1734
1735
  }
1735
1736
  if (cmd === "daemon") {
1736
- run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "daemon", ...rest]);
1737
+ run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), "daemon", ...rest]);
1737
1738
  return;
1738
1739
  }
1739
1740
  if (cmd === "chat") {
@@ -1741,7 +1742,7 @@ async function runCli(argv) {
1741
1742
  if (rest.includes("-g") || rest.includes("--global")) {
1742
1743
  chatArgs.push("-g");
1743
1744
  }
1744
- run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...chatArgs]);
1745
+ run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), ...chatArgs]);
1745
1746
  return;
1746
1747
  }
1747
1748
  if (cmd === "project") {
package/src/code/agent.js CHANGED
@@ -491,6 +491,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
491
491
  const runNativeAgentImpl = typeof options.runNativeAgentImpl === "function"
492
492
  ? options.runNativeAgentImpl
493
493
  : runNativeAgentTask;
494
+ const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
495
+ const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
494
496
  const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => runNativeAgentImpl({
495
497
  workspaceRoot,
496
498
  provider,
@@ -501,6 +503,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
501
503
  sessionId: String(sessionIdValue || ""),
502
504
  timeoutMs: timeoutOverrideMs,
503
505
  onStreamDelta: onStream,
506
+ onThinkingDelta,
507
+ onPhase,
504
508
  onToolEvent: (event) => {
505
509
  pushToolLog(event);
506
510
  },