u-foo 2.5.1 → 2.5.3

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/agents/internal/internalRunner.js +57 -78
  3. package/src/agents/launch/launcher.js +3 -0
  4. package/src/agents/launch/notifier.js +38 -112
  5. package/src/agents/launch/ptyRunner.js +59 -49
  6. package/src/agents/launch/ptyWrapper.js +32 -1
  7. package/src/app/cli/busCoreCommands.js +19 -1
  8. package/src/app/cli/run.js +1 -53
  9. package/src/code/agent.js +41 -140
  10. package/src/coordination/bus/deliveryQueue.js +308 -0
  11. package/src/coordination/bus/index.js +3 -27
  12. package/src/coordination/bus/message.js +35 -15
  13. package/src/coordination/bus/queue.js +23 -7
  14. package/src/runtime/daemon/deliveryScheduler.js +205 -0
  15. package/src/runtime/daemon/index.js +27 -38
  16. package/src/runtime/daemon/ops.js +1 -5
  17. package/src/runtime/daemon/reportControlBus.js +20 -43
  18. package/templates/groups/build-ultra.json +1 -1
  19. package/SKILLS/brandkit/SKILL.md +0 -798
  20. package/SKILLS/brutalist-skill/SKILL.md +0 -92
  21. package/SKILLS/gpt-tasteskill/SKILL.md +0 -74
  22. package/SKILLS/image-to-code-skill/SKILL.md +0 -1228
  23. package/SKILLS/imagegen-frontend-mobile/SKILL.md +0 -1465
  24. package/SKILLS/imagegen-frontend-web/SKILL.md +0 -987
  25. package/SKILLS/llms.txt +0 -13
  26. package/SKILLS/minimalist-skill/SKILL.md +0 -85
  27. package/SKILLS/output-skill/SKILL.md +0 -49
  28. package/SKILLS/redesign-skill/SKILL.md +0 -178
  29. package/SKILLS/soft-skill/SKILL.md +0 -98
  30. package/SKILLS/stitch-skill/DESIGN.md +0 -121
  31. package/SKILLS/stitch-skill/SKILL.md +0 -184
  32. package/SKILLS/taste-skill/SKILL.md +0 -1206
  33. package/SKILLS/taste-skill-v1/SKILL.md +0 -226
  34. package/src/coordination/bus/daemon.js +0 -535
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
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",
@@ -16,6 +16,7 @@ const { redactToolCallPayload, redactSecrets } = require("../../runtime/privacy/
16
16
  const { buildCachedMemoryPrefix } = require("../../coordination/memory");
17
17
  const { normalizePublisher, shouldForwardStreamToPublisher } = require("../launch/publisherRouting");
18
18
  const { appendAgentRegistryDiagnostic } = require("../../coordination/state/agentRegistryDiagnostics");
19
+ const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
19
20
  const {
20
21
  buildDefaultStartupBootstrapPrompt,
21
22
  isValueForCodexOption,
@@ -216,37 +217,15 @@ function shouldStreamReplyToPublisher(projectRoot, publisher, evt = {}) {
216
217
  return shouldForwardStreamToPublisher(projectRoot, publisher);
217
218
  }
218
219
 
219
- function drainQueue(queueFile) {
220
- if (!fs.existsSync(queueFile)) return [];
221
- const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
222
- let content = "";
223
- let readOk = false;
224
- try {
225
- fs.renameSync(queueFile, processingFile);
226
- content = fs.readFileSync(processingFile, "utf8");
227
- readOk = true;
228
- } catch {
229
- try {
230
- if (fs.existsSync(processingFile)) {
231
- fs.renameSync(processingFile, queueFile);
232
- }
233
- } catch {
234
- // ignore rollback errors
235
- }
236
- return [];
237
- } finally {
238
- if (readOk) {
239
- try {
240
- if (fs.existsSync(processingFile)) {
241
- fs.rmSync(processingFile, { force: true });
242
- }
243
- } catch {
244
- // ignore cleanup errors
245
- }
246
- }
220
+ function claimQueuedEvents(queueFile) {
221
+ const queue = new DeliveryQueue(queueFile);
222
+ const claims = [];
223
+ while (true) {
224
+ const claim = queue.claimNext();
225
+ if (!claim) break;
226
+ claims.push({ ...claim, queue });
247
227
  }
248
- if (!content.trim()) return [];
249
- return content.split(/\r?\n/).filter(Boolean);
228
+ return claims;
250
229
  }
251
230
 
252
231
  function parseAgentViewRawInput(message) {
@@ -835,67 +814,67 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
835
814
  if (!processing) {
836
815
  processing = true;
837
816
  try {
838
- const lines = drainQueue(queueFile);
839
- if (lines.length > 0) {
840
- const events = [];
841
- for (const line of lines) {
842
- try {
843
- events.push(JSON.parse(line));
844
- } catch {
845
- // ignore malformed line
846
- }
847
- }
817
+ const claims = claimQueuedEvents(queueFile);
818
+ if (claims.length > 0) {
819
+ let handledAny = false;
848
820
 
849
- const runnableEvents = [];
850
- for (const evt of events) {
821
+ for (const claim of claims) {
822
+ const evt = claim.event;
823
+ const runnableEvents = [];
851
824
  const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
852
825
  if (rawInput === null) {
853
826
  runnableEvents.push(evt);
854
- continue;
827
+ } else {
828
+ const session = getInteractiveSession(evt.publisher || "unknown");
829
+ const submissions = session.handleRaw(rawInput);
830
+ for (const message of submissions) {
831
+ runnableEvents.push({
832
+ ...evt,
833
+ __agentViewRaw: true,
834
+ data: {
835
+ ...(evt.data || {}),
836
+ message,
837
+ },
838
+ });
839
+ }
855
840
  }
856
841
 
857
- const session = getInteractiveSession(evt.publisher || "unknown");
858
- const submissions = session.handleRaw(rawInput);
859
- for (const message of submissions) {
860
- runnableEvents.push({
861
- ...evt,
862
- __agentViewRaw: true,
863
- data: {
864
- ...(evt.data || {}),
865
- message,
866
- },
867
- });
842
+ if (runnableEvents.length > 0) {
843
+ activityTracker.notifyTurnStart("thinking");
868
844
  }
869
- }
870
-
871
- if (runnableEvents.length > 0) {
872
- activityTracker.notifyTurnStart("thinking");
873
- }
874
845
 
875
- for (const evt of runnableEvents) {
876
- // eslint-disable-next-line no-await-in-loop
877
- await handleEvent(
878
- projectRoot,
879
- parsedAgentType,
880
- provider,
881
- model,
882
- subscriber,
883
- nickname,
884
- evt,
885
- busSender,
886
- bootstrap.extraArgs,
887
- threadRuntime,
888
- bootstrap.promptText,
889
- activityTracker
890
- );
891
- if (evt.__agentViewRaw) {
892
- getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
846
+ try {
847
+ for (const runnableEvent of runnableEvents) {
848
+ // eslint-disable-next-line no-await-in-loop
849
+ await handleEvent(
850
+ projectRoot,
851
+ parsedAgentType,
852
+ provider,
853
+ model,
854
+ subscriber,
855
+ nickname,
856
+ runnableEvent,
857
+ busSender,
858
+ bootstrap.extraArgs,
859
+ threadRuntime,
860
+ bootstrap.promptText,
861
+ activityTracker
862
+ );
863
+ if (runnableEvent.__agentViewRaw) {
864
+ getInteractiveSession(runnableEvent.publisher || "unknown").writeResponsePrompt();
865
+ }
866
+ }
867
+ claim.queue.completeClaim(claim);
868
+ if (runnableEvents.length > 0) handledAny = true;
869
+ } catch (err) {
870
+ claim.queue.restoreClaim(claim);
871
+ throw err;
893
872
  }
894
873
  }
895
874
  // 处理消息后更新心跳
896
875
  updateHeartbeat();
897
876
  lastHeartbeat = now;
898
- if (runnableEvents.length > 0) {
877
+ if (handledAny) {
899
878
  activityTracker.markIdle();
900
879
  }
901
880
  await busSender.flush();
@@ -662,6 +662,9 @@ class AgentLauncher {
662
662
  // Enable Claude Code SDK session state events for precise idle/busy detection
663
663
  ...(this.agentType === "claude-code" ? { CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" } : {}),
664
664
  },
665
+ titlePrefix: process.env.UFOO_LAUNCH_MODE === "host"
666
+ ? (this._originalNickname || process.env.UFOO_NICKNAME || "")
667
+ : "",
665
668
  // 未来扩展:ioAdapter: new TerminalIOAdapter()
666
669
  });
667
670
 
@@ -9,8 +9,8 @@ const { shakeTerminalByTty } = require("../../coordination/bus/shake");
9
9
  const { isITerm2 } = require("../../runtime/terminal/detect");
10
10
  const iterm2 = require("../../runtime/terminal/iterm2");
11
11
  const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
12
- const { INJECTION_MODES, getInjectionModeFromEvent } = require("../../coordination/bus/messageMeta");
13
12
  const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
13
+ const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
14
14
 
15
15
  /**
16
16
  * Agent 消息通知监听器
@@ -41,6 +41,7 @@ class AgentNotifier {
41
41
  safeSub,
42
42
  "pending.jsonl"
43
43
  );
44
+ this.deliveryQueue = new DeliveryQueue(this.queueFile);
44
45
  this.agentsFile = paths.agentsFile;
45
46
 
46
47
  // 初始化 injector
@@ -90,7 +91,9 @@ class AgentNotifier {
90
91
  setTitle(nickname) {
91
92
  if (!nickname) return;
92
93
  if (!process.stdout || !process.stdout.isTTY) return;
93
- process.stdout.write(`\x1b]0;${nickname}\x07`);
94
+ if (process.env.UFOO_LAUNCH_MODE !== "host") {
95
+ process.stdout.write(`\x1b]0;${nickname}\x07`);
96
+ }
94
97
  if (isITerm2()) {
95
98
  iterm2.setBadge(nickname);
96
99
  iterm2.setCwd(this.projectRoot);
@@ -187,45 +190,6 @@ class AgentNotifier {
187
190
  }
188
191
  }
189
192
 
190
- drainPending() {
191
- if (!fs.existsSync(this.queueFile)) return [];
192
- const processingFile = `${this.queueFile}.processing.${process.pid}.${Date.now()}`;
193
- let content = "";
194
- let readOk = false;
195
- try {
196
- fs.renameSync(this.queueFile, processingFile);
197
- content = fs.readFileSync(processingFile, "utf8");
198
- readOk = true;
199
- } catch {
200
- try {
201
- if (fs.existsSync(processingFile)) {
202
- fs.renameSync(processingFile, this.queueFile);
203
- }
204
- } catch {
205
- // ignore rollback errors
206
- }
207
- return [];
208
- } finally {
209
- if (readOk) {
210
- try {
211
- if (fs.existsSync(processingFile)) {
212
- fs.rmSync(processingFile, { force: true });
213
- }
214
- } catch {
215
- // ignore cleanup errors
216
- }
217
- }
218
- }
219
- if (!content.trim()) return [];
220
- return content.split(/\r?\n/).filter(Boolean).map((line) => {
221
- try {
222
- return JSON.parse(line);
223
- } catch {
224
- return null;
225
- }
226
- }).filter(Boolean);
227
- }
228
-
229
193
  normalizePublisher(publisher) {
230
194
  if (!publisher) return "";
231
195
  if (typeof publisher === "string") return publisher;
@@ -272,57 +236,41 @@ class AgentNotifier {
272
236
  return 0;
273
237
  }
274
238
 
275
- const events = this.drainPending();
276
- if (events.length === 0) return 0;
277
- const requeue = [];
278
- let delivered = 0;
279
- let consumedOne = false;
280
- for (const evt of events) {
281
- if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
282
- continue;
283
- }
284
- const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
285
- const activityState = this.getCurrentActivityState();
286
- if (injectionMode === INJECTION_MODES.QUEUED && this.isBusyState(activityState)) {
287
- requeue.push(evt);
288
- continue;
289
- }
290
- if (consumedOne) {
291
- requeue.push(evt);
292
- continue;
293
- }
294
- const message = buildPromptInjectionText(evt, this.subscriber, this.getAgentsMap());
295
- try {
296
- // Inject the prompt-facing text into the terminal/tmux agent
297
- // (Bus is the source of truth; inject is the delivery adapter.)
298
- // eslint-disable-next-line no-await-in-loop
299
- await this.injector.inject(this.subscriber, message);
300
- delivered += 1;
301
- consumedOne = true;
302
- this.injectFailCount = 0;
303
- this.updateActivityState("working");
304
- // eslint-disable-next-line no-await-in-loop
305
- await this.emitDelivery(evt, "ok");
306
- } catch (err) {
307
- consumedOne = true;
308
- this.injectFailCount += 1;
309
- requeue.push(evt);
310
- // eslint-disable-next-line no-await-in-loop
311
- await this.emitDelivery(evt, "error", err.message || "inject failed");
312
- }
239
+ if (this.isBusyState(this.getCurrentActivityState())) {
240
+ return 0;
313
241
  }
314
- if (requeue.length > 0) {
315
- try {
316
- const content = requeue.map((e) => JSON.stringify(e)).join("\n") + "\n";
317
- fs.appendFileSync(this.queueFile, content, "utf8");
318
- } catch {
319
- // ignore requeue failures
320
- }
242
+
243
+ const claim = this.deliveryQueue.claimNext();
244
+ if (!claim) return 0;
245
+
246
+ const evt = claim.event;
247
+ if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
248
+ this.deliveryQueue.completeClaim(claim);
249
+ return 0;
250
+ }
251
+
252
+ const activityState = this.getCurrentActivityState();
253
+ if (this.isBusyState(activityState)) {
254
+ this.deliveryQueue.restoreClaim(claim);
255
+ return 0;
321
256
  }
322
- if (delivered > 0) {
257
+
258
+ const message = buildPromptInjectionText(evt, this.subscriber, this.getAgentsMap());
259
+ try {
260
+ // Inject the prompt-facing text into the terminal/tmux agent.
261
+ await this.injector.inject(this.subscriber, message);
262
+ this.deliveryQueue.completeClaim(claim);
263
+ this.injectFailCount = 0;
264
+ this.updateActivityState("working");
265
+ await this.emitDelivery(evt, "ok");
323
266
  this.lastWorkingAt = Date.now();
267
+ return 1;
268
+ } catch (err) {
269
+ this.injectFailCount += 1;
270
+ this.deliveryQueue.restoreClaim(claim);
271
+ await this.emitDelivery(evt, "error", err.message || "inject failed");
272
+ return 0;
324
273
  }
325
- return delivered;
326
274
  }
327
275
 
328
276
  /**
@@ -367,32 +315,10 @@ class AgentNotifier {
367
315
  if (currentCount > this.lastCount) {
368
316
  const newCount = currentCount - this.lastCount;
369
317
  this.notify(newCount);
370
-
371
- // 自动触发终端输入(非阻塞)
372
- this.autoTriggerInput().catch(() => {
373
- // 忽略触发失败
374
- });
375
318
  }
376
319
 
377
- // Ensure pending delivery happens even if count doesn't change
378
- if (this.autoTrigger && currentCount > 0) {
379
- if (this.isUfooCodeSubscriber()) {
380
- if (this.lastUbusWakeCount !== currentCount) {
381
- try {
382
- await this.autoTriggerInput();
383
- this.lastUbusWakeCount = currentCount;
384
- } catch {
385
- // ignore delivery errors
386
- }
387
- }
388
- } else {
389
- try {
390
- await this.deliverPending();
391
- } catch {
392
- // ignore delivery errors
393
- }
394
- }
395
- }
320
+ // Delivery is owned by the project daemon scheduler. The notifier keeps
321
+ // terminal notifications, title badges, heartbeat, and activity fallback.
396
322
  if (currentCount <= 0) {
397
323
  this.lastUbusWakeCount = -1;
398
324
  }
@@ -17,6 +17,7 @@ const {
17
17
  resolveDefaultManualBootstrap,
18
18
  } = require("../prompts/defaultBootstrap");
19
19
  const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
20
+ const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
20
21
 
21
22
  function sleep(ms) {
22
23
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -40,37 +41,15 @@ function safeSubscriber(subscriber) {
40
41
  return subscriber.replace(/:/g, "_");
41
42
  }
42
43
 
43
- function drainQueue(queueFile) {
44
- if (!fs.existsSync(queueFile)) return [];
45
- const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
46
- let content = "";
47
- let readOk = false;
48
- try {
49
- fs.renameSync(queueFile, processingFile);
50
- content = fs.readFileSync(processingFile, "utf8");
51
- readOk = true;
52
- } catch {
53
- try {
54
- if (fs.existsSync(processingFile)) {
55
- fs.renameSync(processingFile, queueFile);
56
- }
57
- } catch {
58
- // ignore rollback errors
59
- }
60
- return [];
61
- } finally {
62
- if (readOk) {
63
- try {
64
- if (fs.existsSync(processingFile)) {
65
- fs.rmSync(processingFile, { force: true });
66
- }
67
- } catch {
68
- // ignore cleanup errors
69
- }
70
- }
44
+ function claimQueuedEvents(queueFile) {
45
+ const queue = new DeliveryQueue(queueFile);
46
+ const claims = [];
47
+ while (true) {
48
+ const claim = queue.claimNext();
49
+ if (!claim) break;
50
+ claims.push({ ...claim, queue });
71
51
  }
72
- if (!content.trim()) return [];
73
- return content.split(/\r?\n/).filter(Boolean);
52
+ return claims;
74
53
  }
75
54
 
76
55
  function stripAnsi(text) {
@@ -79,6 +58,14 @@ function stripAnsi(text) {
79
58
  .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
80
59
  }
81
60
 
61
+ function rewriteTitleOscPrefix(text, prefix) {
62
+ if (!prefix || !text) return text;
63
+ return String(text).replace(/\x1b\]([012]);([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, code, title, terminator) => {
64
+ if (!title || title.startsWith(`${prefix}:`)) return match;
65
+ return `\x1b]${code};${prefix}:${title}${terminator}`;
66
+ });
67
+ }
68
+
82
69
  function parseInputMessage(message) {
83
70
  if (!message) return { raw: false, text: "" };
84
71
  if (parseStreamEnvelope(message)) return null;
@@ -440,8 +427,29 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
440
427
  }
441
428
  }
442
429
 
443
- function broadcastOutput(data) {
430
+ const titlePrefix = process.env.UFOO_LAUNCH_MODE === "host"
431
+ ? String(process.env.UFOO_NICKNAME || "").trim()
432
+ : "";
433
+ let titleOscBuffer = "";
434
+
435
+ function prependTitlePrefix(data) {
436
+ if (!titlePrefix) return Buffer.from(data || "").toString("utf8");
444
437
  const text = Buffer.from(data || "").toString("utf8");
438
+ const combined = titleOscBuffer + text;
439
+ titleOscBuffer = "";
440
+ const trailingOscStart = combined.lastIndexOf("\x1b]");
441
+ if (trailingOscStart >= 0) {
442
+ const trailing = combined.slice(trailingOscStart);
443
+ if (!/(?:\x07|\x1b\\)/.test(trailing)) {
444
+ titleOscBuffer = trailing;
445
+ return rewriteTitleOscPrefix(combined.slice(0, trailingOscStart), titlePrefix);
446
+ }
447
+ }
448
+ return rewriteTitleOscPrefix(combined, titlePrefix);
449
+ }
450
+
451
+ function broadcastOutput(data) {
452
+ const text = prependTitlePrefix(data);
445
453
  if (!text) return;
446
454
  if (process.stdout && process.stdout.isTTY && typeof process.stdout.write === "function") {
447
455
  try {
@@ -1044,28 +1052,29 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1044
1052
  lastHeartbeat = now;
1045
1053
  }
1046
1054
 
1047
- const lines = drainQueue(queueFile);
1048
- if (lines.length > 0) {
1055
+ const claims = claimQueuedEvents(queueFile);
1056
+ if (claims.length > 0) {
1049
1057
  const agents = readAgentsMap(agentsFilePath);
1050
- const events = [];
1051
- for (const line of lines) {
1058
+ for (const claim of claims) {
1059
+ const evt = claim.event;
1052
1060
  try {
1053
- events.push(JSON.parse(line));
1061
+ const input = buildPtyInputFromEvent(evt, subscriber, agents);
1062
+ if (!input) {
1063
+ claim.queue.completeClaim(claim);
1064
+ continue;
1065
+ }
1066
+ const { raw, text } = input;
1067
+ if (messageQueue.length >= maxQueue) {
1068
+ messageQueue.shift();
1069
+ }
1070
+ const publisher = typeof evt.publisher === "object" && evt.publisher
1071
+ ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
1072
+ : (evt.publisher || "unknown");
1073
+ messageQueue.push({ publisher, raw, text });
1074
+ claim.queue.completeClaim(claim);
1054
1075
  } catch {
1055
- // ignore malformed line
1056
- }
1057
- }
1058
- for (const evt of events) {
1059
- const input = buildPtyInputFromEvent(evt, subscriber, agents);
1060
- if (!input) continue;
1061
- const { raw, text } = input;
1062
- if (messageQueue.length >= maxQueue) {
1063
- messageQueue.shift();
1076
+ claim.queue.restoreClaim(claim);
1064
1077
  }
1065
- const publisher = typeof evt.publisher === "object" && evt.publisher
1066
- ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
1067
- : (evt.publisher || "unknown");
1068
- messageQueue.push({ publisher, raw, text });
1069
1078
  }
1070
1079
  }
1071
1080
  processQueue();
@@ -1078,6 +1087,7 @@ module.exports = {
1078
1087
  appendStartupBootstrapArg,
1079
1088
  buildPtyInputFromEvent,
1080
1089
  parseInputMessage,
1090
+ rewriteTitleOscPrefix,
1081
1091
  resolvePtyBootstrapArgs,
1082
1092
  resolveCommand,
1083
1093
  runPtyRunner,
@@ -41,6 +41,8 @@ class PtyWrapper {
41
41
 
42
42
  // 可插拔的IO适配器(未来扩展)
43
43
  this.ioAdapter = options.ioAdapter || null;
44
+ this.titlePrefix = String(options.titlePrefix || "").trim();
45
+ this._titleOscBuffer = "";
44
46
 
45
47
  // 事件处理器引用(用于cleanup)
46
48
  this._stdinHandler = null;
@@ -111,8 +113,10 @@ class PtyWrapper {
111
113
  _attachDirectStreams(stdin, stdout) {
112
114
  // PTY输出 -> stdout
113
115
  this._ptyDataHandler = (data) => {
116
+ const output = this._prependTitlePrefix(data);
117
+
114
118
  // 1. 输出到terminal
115
- stdout.write(data);
119
+ stdout.write(output);
116
120
 
117
121
  // 2. 可选:日志记录(JSONL格式)
118
122
  if (this.logger) {
@@ -185,6 +189,33 @@ class PtyWrapper {
185
189
  this.pty.onExit(this._ptyExitHandler);
186
190
  }
187
191
 
192
+ _prependTitlePrefix(data) {
193
+ if (!this.titlePrefix) return data;
194
+ const text = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
195
+ const combined = this._titleOscBuffer + text;
196
+ this._titleOscBuffer = "";
197
+
198
+ const trailingOscStart = combined.lastIndexOf("\x1b]");
199
+ if (trailingOscStart >= 0) {
200
+ const trailing = combined.slice(trailingOscStart);
201
+ if (!/(?:\x07|\x1b\\)/.test(trailing)) {
202
+ this._titleOscBuffer = trailing;
203
+ return this._rewriteTitleOsc(combined.slice(0, trailingOscStart));
204
+ }
205
+ }
206
+
207
+ return this._rewriteTitleOsc(combined);
208
+ }
209
+
210
+ _rewriteTitleOsc(text) {
211
+ if (!this.titlePrefix || !text) return text;
212
+ const prefix = this.titlePrefix;
213
+ return text.replace(/\x1b\]([012]);([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, code, title, terminator) => {
214
+ if (!title || title.startsWith(`${prefix}:`)) return match;
215
+ return `\x1b]${code};${prefix}:${title}${terminator}`;
216
+ });
217
+ }
218
+
188
219
  /**
189
220
  * 写入数据到PTY(用于外部inject)
190
221
  *
@@ -39,6 +39,18 @@ function parseSendArgs(cmdArgs = []) {
39
39
  };
40
40
  }
41
41
 
42
+ function resolvePollSubscriber(cmdArgs = [], env = process.env) {
43
+ const positionals = cmdArgs.filter((arg) => typeof arg === "string" && !arg.startsWith("--"));
44
+ const subscriber = String(positionals[0] || env.UFOO_SUBSCRIBER_ID || "").trim();
45
+ if (!subscriber) {
46
+ throw new Error("poll requires [subscriber] or UFOO_SUBSCRIBER_ID");
47
+ }
48
+ return {
49
+ subscriber,
50
+ autoAck: cmdArgs.includes("--ack") || cmdArgs.includes("--auto-ack"),
51
+ };
52
+ }
53
+
42
54
  async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
43
55
  switch (cmd) {
44
56
  case "init":
@@ -76,6 +88,12 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
76
88
  case "check":
77
89
  await eventBus.check(cmdArgs[0]);
78
90
  return {};
91
+ case "poll":
92
+ {
93
+ const parsed = resolvePollSubscriber(cmdArgs);
94
+ await eventBus.check(parsed.subscriber, parsed.autoAck);
95
+ }
96
+ return {};
79
97
  case "ack":
80
98
  await eventBus.ack(cmdArgs[0]);
81
99
  return {};
@@ -99,4 +117,4 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
99
117
  }
100
118
  }
101
119
 
102
- module.exports = { runBusCoreCommand };
120
+ module.exports = { runBusCoreCommand, resolvePollSubscriber };