u-foo 1.0.6 → 1.1.9

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 (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,103 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const crypto = require("crypto");
5
+
6
+ function defaultTokensPath() {
7
+ return path.join(os.homedir(), ".ufoo", "online", "tokens.json");
8
+ }
9
+
10
+ function normalizeTokensData(raw) {
11
+ if (!raw || typeof raw !== "object") {
12
+ return { agents: {} };
13
+ }
14
+ if (raw.agents && typeof raw.agents === "object") {
15
+ return { agents: raw.agents };
16
+ }
17
+ // Legacy: flat object mapping id -> token
18
+ return { agents: raw };
19
+ }
20
+
21
+ function generateToken(bytes = 24) {
22
+ return crypto.randomBytes(bytes).toString("base64url");
23
+ }
24
+
25
+ function hashToken(token) {
26
+ if (!token) return "";
27
+ return crypto.createHash("sha256").update(String(token)).digest("hex");
28
+ }
29
+
30
+ function loadTokens(filePath = defaultTokensPath()) {
31
+ try {
32
+ const raw = fs.readFileSync(filePath, "utf8");
33
+ const parsed = JSON.parse(raw);
34
+ return normalizeTokensData(parsed);
35
+ } catch {
36
+ return { agents: {} };
37
+ }
38
+ }
39
+
40
+ function saveTokens(filePath = defaultTokensPath(), data = { agents: {} }) {
41
+ const dir = path.dirname(filePath);
42
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
43
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
44
+ }
45
+
46
+ function setToken(filePath, agentId, token, server = "", extra = {}) {
47
+ if (!agentId || !token) throw new Error("agentId and token are required");
48
+ const data = loadTokens(filePath);
49
+ data.agents[agentId] = {
50
+ token,
51
+ token_hash: hashToken(token),
52
+ server,
53
+ nickname: extra.nickname || data.agents[agentId]?.nickname || "",
54
+ updated_at: new Date().toISOString(),
55
+ };
56
+ saveTokens(filePath, data);
57
+ return data.agents[agentId];
58
+ }
59
+
60
+ function removeToken(filePath, agentId) {
61
+ const data = loadTokens(filePath);
62
+ if (data.agents[agentId]) {
63
+ delete data.agents[agentId];
64
+ saveTokens(filePath, data);
65
+ }
66
+ return data;
67
+ }
68
+
69
+ function getToken(filePath, agentId) {
70
+ const data = loadTokens(filePath);
71
+ return data.agents[agentId] || null;
72
+ }
73
+
74
+ function getTokenByNickname(filePath, nickname) {
75
+ const data = loadTokens(filePath);
76
+ let best = null;
77
+ for (const entry of Object.values(data.agents)) {
78
+ if (entry.nickname === nickname) {
79
+ if (!best || (entry.updated_at || "") > (best.updated_at || "")) {
80
+ best = entry;
81
+ }
82
+ }
83
+ }
84
+ return best;
85
+ }
86
+
87
+ function listTokens(filePath) {
88
+ const data = loadTokens(filePath);
89
+ return Object.entries(data.agents).map(([id, entry]) => ({ id, ...entry }));
90
+ }
91
+
92
+ module.exports = {
93
+ defaultTokensPath,
94
+ generateToken,
95
+ hashToken,
96
+ loadTokens,
97
+ saveTokens,
98
+ setToken,
99
+ removeToken,
100
+ getToken,
101
+ getTokenByNickname,
102
+ listTokens,
103
+ };
@@ -0,0 +1,331 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { getUfooPaths } = require("../ufoo/paths");
4
+
5
+ const REPORT_PHASES = {
6
+ START: "start",
7
+ PROGRESS: "progress",
8
+ DONE: "done",
9
+ ERROR: "error",
10
+ };
11
+
12
+ function getReportPaths(projectRoot) {
13
+ const { agentDir } = getUfooPaths(projectRoot);
14
+ return {
15
+ reportsFile: path.join(agentDir, "reports.jsonl"),
16
+ stateFile: path.join(agentDir, "report-state.json"),
17
+ };
18
+ }
19
+
20
+ function ensureReportDir(projectRoot) {
21
+ const { agentDir } = getUfooPaths(projectRoot);
22
+ fs.mkdirSync(agentDir, { recursive: true });
23
+ }
24
+
25
+ function normalizePhase(value = "") {
26
+ const phase = String(value || "").trim().toLowerCase();
27
+ if (phase === REPORT_PHASES.START) return REPORT_PHASES.START;
28
+ if (phase === REPORT_PHASES.PROGRESS) return REPORT_PHASES.PROGRESS;
29
+ if (phase === REPORT_PHASES.ERROR) return REPORT_PHASES.ERROR;
30
+ return REPORT_PHASES.DONE;
31
+ }
32
+
33
+ function normalizeScope(value = "") {
34
+ const scope = String(value || "").trim().toLowerCase();
35
+ if (scope === "private") return "private";
36
+ return "public";
37
+ }
38
+
39
+ function normalizeReportInput(input = {}, options = {}) {
40
+ const ts = options.ts || new Date().toISOString();
41
+ let phase = normalizePhase(input.phase || options.phase);
42
+ const entryId = String(
43
+ input.entry_id
44
+ || input.entryId
45
+ || options.entry_id
46
+ || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
47
+ ).trim();
48
+
49
+ const taskId = String(
50
+ input.task_id
51
+ || input.taskId
52
+ || input.task
53
+ || options.task_id
54
+ || options.taskId
55
+ || `task-${Date.now()}`
56
+ ).trim();
57
+ const agentId = String(
58
+ input.agent_id
59
+ || input.agentId
60
+ || options.agent_id
61
+ || options.agentId
62
+ || "unknown-agent"
63
+ ).trim() || "unknown-agent";
64
+ const message = String(input.message || "").trim();
65
+ const summary = String(input.summary || "").trim();
66
+ const error = String(input.error || "").trim();
67
+ const source = String(input.source || options.source || "cli").trim() || "cli";
68
+ const scope = normalizeScope(input.scope || options.scope);
69
+ const controllerId = String(
70
+ input.controller_id
71
+ || input.controllerId
72
+ || options.controller_id
73
+ || options.controllerId
74
+ || "ufoo-agent"
75
+ ).trim() || "ufoo-agent";
76
+ const ok = input.ok !== false && phase !== REPORT_PHASES.ERROR && !error;
77
+ if (!ok && phase !== REPORT_PHASES.START && phase !== REPORT_PHASES.PROGRESS) {
78
+ phase = REPORT_PHASES.ERROR;
79
+ }
80
+
81
+ const meta = input.meta && typeof input.meta === "object" ? input.meta : {};
82
+
83
+ return {
84
+ entry_id: entryId,
85
+ ts,
86
+ phase,
87
+ task_id: taskId,
88
+ agent_id: agentId,
89
+ message,
90
+ summary,
91
+ error,
92
+ ok,
93
+ source,
94
+ scope,
95
+ controller_id: controllerId,
96
+ meta,
97
+ };
98
+ }
99
+
100
+ function appendReport(projectRoot, entry) {
101
+ ensureReportDir(projectRoot);
102
+ const { reportsFile } = getReportPaths(projectRoot);
103
+ fs.appendFileSync(reportsFile, `${JSON.stringify(entry)}\n`, "utf8");
104
+ }
105
+
106
+ function parseJsonLines(file) {
107
+ if (!fs.existsSync(file)) return [];
108
+ const raw = fs.readFileSync(file, "utf8");
109
+ if (!raw.trim()) return [];
110
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
111
+ const rows = [];
112
+ for (const line of lines) {
113
+ try {
114
+ rows.push(JSON.parse(line));
115
+ } catch {
116
+ // ignore malformed lines
117
+ }
118
+ }
119
+ return rows;
120
+ }
121
+
122
+ function listReports(projectRoot, options = {}) {
123
+ const { reportsFile } = getReportPaths(projectRoot);
124
+ const num = Number.isFinite(options.num) && options.num > 0 ? options.num : 20;
125
+ const filterAgent = String(options.agent || options.agent_id || "").trim();
126
+ let rows = parseJsonLines(reportsFile);
127
+ if (filterAgent) {
128
+ rows = rows.filter((row) => String(row.agent_id || "").trim() === filterAgent);
129
+ }
130
+ rows.sort((a, b) => {
131
+ const left = new Date(a.ts || 0).getTime();
132
+ const right = new Date(b.ts || 0).getTime();
133
+ return right - left;
134
+ });
135
+ return rows.slice(0, num);
136
+ }
137
+
138
+ function loadReportState(projectRoot) {
139
+ const { stateFile } = getReportPaths(projectRoot);
140
+ try {
141
+ const parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
142
+ if (!parsed || typeof parsed !== "object") return { updated_at: "", agents: {} };
143
+ if (!parsed.agents || typeof parsed.agents !== "object") parsed.agents = {};
144
+ if (typeof parsed.updated_at !== "string") parsed.updated_at = "";
145
+ return parsed;
146
+ } catch {
147
+ return { updated_at: "", agents: {} };
148
+ }
149
+ }
150
+
151
+ function saveReportState(projectRoot, state) {
152
+ ensureReportDir(projectRoot);
153
+ const { stateFile } = getReportPaths(projectRoot);
154
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
155
+ }
156
+
157
+ function updateReportState(projectRoot, entry) {
158
+ const state = loadReportState(projectRoot);
159
+ const current = state.agents[entry.agent_id] && typeof state.agents[entry.agent_id] === "object"
160
+ ? state.agents[entry.agent_id]
161
+ : {};
162
+ const pending = current.pending && typeof current.pending === "object"
163
+ ? { ...current.pending }
164
+ : {};
165
+
166
+ if (entry.phase === REPORT_PHASES.START || entry.phase === REPORT_PHASES.PROGRESS) {
167
+ pending[entry.task_id] = entry;
168
+ } else {
169
+ delete pending[entry.task_id];
170
+ }
171
+
172
+ state.agents[entry.agent_id] = {
173
+ ...current,
174
+ pending,
175
+ pending_count: Object.keys(pending).length,
176
+ last: entry,
177
+ updated_at: entry.ts,
178
+ };
179
+ state.updated_at = entry.ts;
180
+ saveReportState(projectRoot, state);
181
+ return state;
182
+ }
183
+
184
+ function isSummaryHiddenEntry(entry) {
185
+ if (!entry || typeof entry !== "object") return false;
186
+ const scope = normalizeScope(entry.scope);
187
+ if (scope !== "private") return false;
188
+ const controllerId = String(entry.controller_id || "").trim();
189
+ return controllerId === "ufoo-agent";
190
+ }
191
+
192
+ function controllerToSafeName(controllerId = "") {
193
+ return String(controllerId || "ufoo-agent")
194
+ .trim()
195
+ .replace(/[^a-zA-Z0-9._-]/g, "_");
196
+ }
197
+
198
+ function getControllerInboxFile(projectRoot, controllerId = "ufoo-agent") {
199
+ const { agentDir } = getUfooPaths(projectRoot);
200
+ const inboxDir = path.join(agentDir, "private-inbox");
201
+ const safe = controllerToSafeName(controllerId);
202
+ return path.join(inboxDir, `${safe}.jsonl`);
203
+ }
204
+
205
+ function appendControllerInboxEntry(projectRoot, controllerId, entry) {
206
+ const file = getControllerInboxFile(projectRoot, controllerId);
207
+ fs.mkdirSync(path.dirname(file), { recursive: true });
208
+ fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, "utf8");
209
+ }
210
+
211
+ function listControllerInboxEntries(projectRoot, controllerId = "ufoo-agent", options = {}) {
212
+ const file = getControllerInboxFile(projectRoot, controllerId);
213
+ const rows = parseJsonLines(file);
214
+ const num = Number.isFinite(options.num) && options.num > 0 ? options.num : rows.length;
215
+ if (rows.length <= num) return rows;
216
+ return rows.slice(rows.length - num);
217
+ }
218
+
219
+ function clearControllerInbox(projectRoot, controllerId = "ufoo-agent") {
220
+ const file = getControllerInboxFile(projectRoot, controllerId);
221
+ try {
222
+ if (fs.existsSync(file)) fs.rmSync(file, { force: true });
223
+ } catch {
224
+ // ignore clear errors
225
+ }
226
+ }
227
+
228
+ function consumeControllerInboxEntries(projectRoot, controllerId = "ufoo-agent", consumed = []) {
229
+ const file = getControllerInboxFile(projectRoot, controllerId);
230
+ if (!fs.existsSync(file)) return { removed: 0, remaining: 0 };
231
+ const list = Array.isArray(consumed) ? consumed : [];
232
+ if (list.length === 0) {
233
+ const current = parseJsonLines(file);
234
+ return { removed: 0, remaining: current.length };
235
+ }
236
+
237
+ const idSet = new Set();
238
+ const legacySerialized = new Set();
239
+ for (const item of list) {
240
+ if (!item) continue;
241
+ if (typeof item === "string") {
242
+ idSet.add(item);
243
+ continue;
244
+ }
245
+ if (typeof item === "object") {
246
+ if (item.entry_id) {
247
+ idSet.add(String(item.entry_id));
248
+ } else {
249
+ legacySerialized.add(JSON.stringify(item));
250
+ }
251
+ }
252
+ }
253
+
254
+ const raw = fs.readFileSync(file, "utf8");
255
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
256
+ if (lines.length === 0) {
257
+ return { removed: 0, remaining: 0 };
258
+ }
259
+
260
+ const keptLines = [];
261
+ let removed = 0;
262
+ for (const line of lines) {
263
+ let parsed = null;
264
+ try {
265
+ parsed = JSON.parse(line);
266
+ } catch {
267
+ parsed = null;
268
+ }
269
+ if (parsed && typeof parsed === "object") {
270
+ const id = parsed.entry_id ? String(parsed.entry_id) : "";
271
+ if (id && idSet.has(id)) {
272
+ removed += 1;
273
+ continue;
274
+ }
275
+ if (!id && legacySerialized.has(JSON.stringify(parsed))) {
276
+ removed += 1;
277
+ continue;
278
+ }
279
+ }
280
+ keptLines.push(line);
281
+ }
282
+
283
+ if (keptLines.length === 0) {
284
+ fs.rmSync(file, { force: true });
285
+ return { removed, remaining: 0 };
286
+ }
287
+
288
+ fs.writeFileSync(file, `${keptLines.join("\n")}\n`, "utf8");
289
+ return { removed, remaining: keptLines.length };
290
+ }
291
+
292
+ function readReportSummary(projectRoot) {
293
+ const state = loadReportState(projectRoot);
294
+ const agents = Object.entries(state.agents || {}).map(([agentId, value]) => {
295
+ const pending = value && value.pending && typeof value.pending === "object"
296
+ ? Object.values(value.pending).filter((entry) => !isSummaryHiddenEntry(entry))
297
+ : [];
298
+ return {
299
+ agent_id: agentId,
300
+ pending_count: pending.length,
301
+ pending,
302
+ last: value && value.last ? value.last : null,
303
+ updated_at: value && value.updated_at ? value.updated_at : "",
304
+ };
305
+ });
306
+ return {
307
+ updated_at: state.updated_at || "",
308
+ pending_total: agents.reduce((sum, item) => sum + item.pending_count, 0),
309
+ agents,
310
+ };
311
+ }
312
+
313
+ module.exports = {
314
+ REPORT_PHASES,
315
+ getReportPaths,
316
+ normalizePhase,
317
+ normalizeScope,
318
+ normalizeReportInput,
319
+ appendReport,
320
+ listReports,
321
+ loadReportState,
322
+ saveReportState,
323
+ updateReportState,
324
+ readReportSummary,
325
+ controllerToSafeName,
326
+ getControllerInboxFile,
327
+ appendControllerInboxEntry,
328
+ listControllerInboxEntries,
329
+ clearControllerInbox,
330
+ consumeControllerInboxEntries,
331
+ };
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ const IPC_REQUEST_TYPES = {
4
+ STATUS: "status",
5
+ PROMPT: "prompt",
6
+ BUS_SEND: "bus_send",
7
+ CLOSE_AGENT: "close_agent",
8
+ LAUNCH_AGENT: "launch_agent",
9
+ RESUME_AGENTS: "resume_agents",
10
+ LIST_RECOVERABLE_AGENTS: "list_recoverable_agents",
11
+ REGISTER_AGENT: "register_agent",
12
+ AGENT_READY: "agent_ready",
13
+ AGENT_REPORT: "agent_report",
14
+ };
15
+
16
+ const IPC_RESPONSE_TYPES = {
17
+ STATUS: "status",
18
+ RESPONSE: "response",
19
+ BUS: "bus",
20
+ ERROR: "error",
21
+ BUS_SEND_OK: "bus_send_ok",
22
+ REGISTER_OK: "register_ok",
23
+ };
24
+
25
+ const BUS_STATUS_PHASES = {
26
+ START: "start",
27
+ DONE: "done",
28
+ ERROR: "error",
29
+ };
30
+
31
+ module.exports = {
32
+ IPC_REQUEST_TYPES,
33
+ IPC_RESPONSE_TYPES,
34
+ BUS_STATUS_PHASES,
35
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ const PTY_SOCKET_MESSAGE_TYPES = {
4
+ OUTPUT: "output",
5
+ REPLAY: "replay",
6
+ SNAPSHOT: "snapshot",
7
+ SUBSCRIBED: "subscribed",
8
+ SUBSCRIBE: "subscribe",
9
+ RAW: "raw",
10
+ RESIZE: "resize",
11
+ };
12
+
13
+ const PTY_SOCKET_SUBSCRIBE_MODES = {
14
+ FULL: "full",
15
+ SCREEN: "screen",
16
+ };
17
+
18
+ module.exports = {
19
+ PTY_SOCKET_MESSAGE_TYPES,
20
+ PTY_SOCKET_SUBSCRIBE_MODES,
21
+ };
@@ -1,8 +1,52 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const childProcess = require("child_process");
3
4
  const { readJSON } = require("../bus/utils");
4
5
  const { getUfooPaths } = require("../ufoo/paths");
5
6
 
7
+ function normalizeTty(ttyPath) {
8
+ if (!ttyPath) return "";
9
+ const trimmed = String(ttyPath).trim();
10
+ if (!trimmed || trimmed === "not a tty") return "";
11
+ if (trimmed === "/dev/tty") return "";
12
+ return trimmed;
13
+ }
14
+
15
+ function tryTtyWithFd(fd) {
16
+ try {
17
+ const res = childProcess.spawnSync("tty", {
18
+ stdio: [fd, "pipe", "ignore"],
19
+ encoding: "utf8",
20
+ });
21
+ if (res && res.status === 0) {
22
+ const tty = normalizeTty(res.stdout || "");
23
+ if (tty) return tty;
24
+ }
25
+ } catch {
26
+ // ignore
27
+ }
28
+ return "";
29
+ }
30
+
31
+ function detectCurrentTty() {
32
+ const stdinTtyPath = normalizeTty(process.stdin?.ttyPath || "");
33
+ if (stdinTtyPath) return stdinTtyPath;
34
+
35
+ const fromStdin = tryTtyWithFd(0);
36
+ if (fromStdin) return fromStdin;
37
+
38
+ try {
39
+ const fd = fs.openSync("/dev/tty", "r");
40
+ const fromTty = tryTtyWithFd(fd);
41
+ fs.closeSync(fd);
42
+ if (fromTty) return fromTty;
43
+ } catch {
44
+ // ignore
45
+ }
46
+
47
+ return "";
48
+ }
49
+
6
50
  /**
7
51
  * 显示项目状态
8
52
  */
@@ -38,12 +82,7 @@ class StatusDisplay {
38
82
  }
39
83
 
40
84
  // 尝试通过 tty 查找订阅者
41
- let currentTty = null;
42
- try {
43
- currentTty = fs.readFileSync("/dev/tty", "utf8").trim();
44
- } catch {
45
- // tty 不可用
46
- }
85
+ const currentTty = detectCurrentTty();
47
86
 
48
87
  if (currentTty && currentTty.startsWith("/dev/")) {
49
88
  const busData = readJSON(agentsFile);
@@ -198,21 +237,15 @@ class StatusDisplay {
198
237
  }
199
238
 
200
239
  /**
201
- * 显示横幅(如果存在)
240
+ * 显示横幅
202
241
  */
203
242
  showBanner(subscriber) {
204
- if (!subscriber) {
205
- console.log("=== ufoo status ===");
243
+ console.log("=== ufoo status ===");
244
+ if (subscriber) {
245
+ console.log(`Agent: ${subscriber}`);
246
+ } else {
206
247
  console.log();
207
- return;
208
248
  }
209
-
210
- const { showBanner } = require("../utils/banner");
211
- const agentType = subscriber.startsWith("codex:") ? "codex" : "claude";
212
- const sessionId = subscriber.split(":")[1] || "unknown";
213
- const nickname = this.getSubscriberNickname(subscriber);
214
-
215
- showBanner({ agentType, sessionId, nickname });
216
249
  }
217
250
 
218
251
  /**
@@ -0,0 +1,87 @@
1
+ const TERMINAL_CAPABILITY_KEYS = [
2
+ "supportsActivate",
3
+ "supportsSubscribeFull",
4
+ "supportsSubscribeScreen",
5
+ "supportsSnapshot",
6
+ "supportsReplay",
7
+ "supportsWindowClose",
8
+ "supportsSocketProtocol",
9
+ "supportsNotifierInjector",
10
+ "supportsInternalQueueLoop",
11
+ "supportsRestartFallback",
12
+ "supportsSessionReuse",
13
+ ];
14
+
15
+ const TERMINAL_ADAPTER_METHODS = [
16
+ "connect",
17
+ "disconnect",
18
+ "send",
19
+ "sendRaw",
20
+ "resize",
21
+ "snapshot",
22
+ "subscribe",
23
+ "activate",
24
+ "getState",
25
+ ];
26
+
27
+ function createTerminalCapabilities(overrides = {}) {
28
+ const capabilities = {};
29
+ for (const key of TERMINAL_CAPABILITY_KEYS) {
30
+ capabilities[key] = false;
31
+ }
32
+ return { ...capabilities, ...overrides };
33
+ }
34
+
35
+ function assertTerminalCapabilities(capabilities) {
36
+ if (!capabilities || typeof capabilities !== "object") {
37
+ throw new Error("TerminalAdapter capabilities must be an object");
38
+ }
39
+ for (const key of TERMINAL_CAPABILITY_KEYS) {
40
+ if (!(key in capabilities)) {
41
+ throw new Error(`TerminalAdapter capabilities missing: ${key}`);
42
+ }
43
+ if (typeof capabilities[key] !== "boolean") {
44
+ throw new Error(`TerminalAdapter capability must be boolean: ${key}`);
45
+ }
46
+ }
47
+ return true;
48
+ }
49
+
50
+ function createUnsupportedCapabilityError(capability, operation) {
51
+ const suffix = operation ? ` (operation: ${operation})` : "";
52
+ const err = new Error(`TerminalAdapter capability unsupported: ${capability}${suffix}`);
53
+ err.code = "UFOO_UNSUPPORTED_CAPABILITY";
54
+ err.capability = capability;
55
+ err.operation = operation || null;
56
+ return err;
57
+ }
58
+
59
+ function requireCapability(capabilities, capability, operation) {
60
+ if (!capabilities || !capabilities[capability]) {
61
+ throw createUnsupportedCapabilityError(capability, operation);
62
+ }
63
+ return true;
64
+ }
65
+
66
+ function assertTerminalAdapterContract(adapter) {
67
+ if (!adapter || typeof adapter !== "object") {
68
+ throw new Error("TerminalAdapter must be an object");
69
+ }
70
+ for (const method of TERMINAL_ADAPTER_METHODS) {
71
+ if (typeof adapter[method] !== "function") {
72
+ throw new Error(`TerminalAdapter missing method: ${method}`);
73
+ }
74
+ }
75
+ assertTerminalCapabilities(adapter.capabilities);
76
+ return true;
77
+ }
78
+
79
+ module.exports = {
80
+ TERMINAL_CAPABILITY_KEYS,
81
+ TERMINAL_ADAPTER_METHODS,
82
+ createTerminalCapabilities,
83
+ assertTerminalCapabilities,
84
+ createUnsupportedCapabilityError,
85
+ requireCapability,
86
+ assertTerminalAdapterContract,
87
+ };