u-foo 1.0.3 → 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 (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -0,0 +1,306 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const EventBus = require("../bus");
5
+ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
6
+ const { getUfooPaths } = require("../ufoo/paths");
7
+
8
+ /**
9
+ * Build probe marker using nickname (e.g., "claude-47")
10
+ * Simpler than the old token format, easier to search
11
+ */
12
+ function buildProbeMarker(nickname) {
13
+ return nickname || "";
14
+ }
15
+
16
+ /**
17
+ * Build probe command:
18
+ * - claude-code: /ufoo <nickname>
19
+ * - codex: $ufoo <nickname>
20
+ */
21
+ function buildProbeCommand(agentType, nickname) {
22
+ const marker = String(nickname || "").trim();
23
+ if (agentType === "claude-code") {
24
+ return `/ufoo ${marker}`;
25
+ }
26
+ return `$ufoo ${marker}`;
27
+ }
28
+
29
+ function readLines(filePath) {
30
+ try {
31
+ const raw = fs.readFileSync(filePath, "utf8");
32
+ return raw.split(/\r?\n/).filter(Boolean);
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ function escapeRegExp(value = "") {
39
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ }
41
+
42
+ function containsProbeCommand(text, marker) {
43
+ if (!text || !marker) return false;
44
+ const escapedMarker = escapeRegExp(marker);
45
+ const pattern = `(?:^|[\\s"'\\\`])(?:\\/ufoo|\\$ufoo|ufoo)\\s+${escapedMarker}(?=$|[\\s"'\\\`.,:;!?\\]\\)\\}])`;
46
+ const re = new RegExp(pattern);
47
+ return re.test(String(text));
48
+ }
49
+
50
+ /**
51
+ * Check if a history record contains our probe marker
52
+ * Searches for probe marker command patterns:
53
+ * - "/ufoo <marker>" (claude)
54
+ * - "$ufoo <marker>" (codex)
55
+ * - "ufoo <marker>" (legacy compatibility)
56
+ */
57
+ function recordContainsMarker(record, marker, rawLine) {
58
+ if (!marker) return false;
59
+
60
+ // Check raw line first (fastest)
61
+ if (containsProbeCommand(rawLine, marker)) return true;
62
+
63
+ if (!record || typeof record !== "object") return false;
64
+
65
+ // Check common fields where user input might appear
66
+ const fields = [
67
+ record.display, // history.jsonl uses "display" for user input
68
+ record.text,
69
+ record.prompt,
70
+ record.input,
71
+ record.message,
72
+ record.query,
73
+ record.content,
74
+ ];
75
+
76
+ for (const field of fields) {
77
+ if (containsProbeCommand(field, marker)) return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ function extractSessionId(record, rawLine) {
83
+ if (record && typeof record === "object") {
84
+ return record.session_id || record.sessionId || record.session || "";
85
+ }
86
+ if (typeof rawLine === "string") {
87
+ const match = rawLine.match(/"session(?:_id|Id)"\s*:\s*"([^"]+)"/);
88
+ if (match && match[1]) return match[1];
89
+ }
90
+ return "";
91
+ }
92
+
93
+ /**
94
+ * Find session ID in a history file by searching for the probe marker
95
+ */
96
+ function findSessionInFile(filePath, marker) {
97
+ if (!filePath || !fs.existsSync(filePath)) return null;
98
+ const lines = readLines(filePath);
99
+
100
+ // Search from end (most recent first)
101
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
102
+ const line = lines[i];
103
+
104
+ // Quick check: line must contain the marker string
105
+ if (!line.includes(marker)) continue;
106
+
107
+ let record = null;
108
+ try {
109
+ record = JSON.parse(line);
110
+ } catch {
111
+ record = null;
112
+ }
113
+
114
+ if (!recordContainsMarker(record, marker, line)) continue;
115
+
116
+ const sessionId = extractSessionId(record, line);
117
+ if (sessionId) {
118
+ return { sessionId, source: filePath };
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+
124
+ function getClaudeHistoryPath() {
125
+ return path.join(os.homedir(), ".claude", "history.jsonl");
126
+ }
127
+
128
+ function getCodexHistoryPath() {
129
+ return path.join(os.homedir(), ".codex", "history.jsonl");
130
+ }
131
+
132
+ /**
133
+ * Search provider history for the probe marker and return session ID
134
+ */
135
+ function resolveProviderSession(agentType, marker) {
136
+ if (agentType === "codex") {
137
+ return findSessionInFile(getCodexHistoryPath(), marker);
138
+ }
139
+ if (agentType === "claude-code") {
140
+ return findSessionInFile(getClaudeHistoryPath(), marker);
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Save probe marker to agents data (for debugging/tracking)
147
+ */
148
+ function persistProbeMarker(projectRoot, subscriberId, marker) {
149
+ const filePath = getUfooPaths(projectRoot).agentsFile;
150
+ const data = loadAgentsData(filePath);
151
+ const meta = data.agents[subscriberId] || {};
152
+ data.agents[subscriberId] = {
153
+ ...meta,
154
+ provider_session_probe: marker,
155
+ provider_session_updated_at: new Date().toISOString(),
156
+ };
157
+ saveAgentsData(filePath, data);
158
+ }
159
+
160
+ function persistProviderSession(projectRoot, subscriberId, payload) {
161
+ const filePath = getUfooPaths(projectRoot).agentsFile;
162
+ const data = loadAgentsData(filePath);
163
+ const meta = data.agents[subscriberId] || {};
164
+ data.agents[subscriberId] = {
165
+ ...meta,
166
+ provider_session_id: payload.sessionId || "",
167
+ provider_session_source: payload.source || "",
168
+ provider_session_updated_at: new Date().toISOString(),
169
+ };
170
+ saveAgentsData(filePath, data);
171
+ }
172
+
173
+ /**
174
+ * Retry searching for session ID with the given marker
175
+ */
176
+ async function resolveWithRetries(agentType, marker, attempts = 12, intervalMs = 2000) {
177
+ for (let i = 0; i < attempts; i += 1) {
178
+ const resolved = resolveProviderSession(agentType, marker);
179
+ if (resolved && resolved.sessionId) return resolved;
180
+ // eslint-disable-next-line no-await-in-loop
181
+ await new Promise((r) => setTimeout(r, intervalMs));
182
+ }
183
+ return null;
184
+ }
185
+
186
+
187
+ function loadProviderSessionCache(projectRoot) {
188
+ const filePath = getUfooPaths(projectRoot).agentsFile;
189
+ const data = loadAgentsData(filePath);
190
+ const cache = new Map();
191
+ for (const [id, meta] of Object.entries(data.agents || {})) {
192
+ if (meta && meta.provider_session_id) {
193
+ cache.set(id, {
194
+ sessionId: meta.provider_session_id,
195
+ source: meta.provider_session_source || "",
196
+ updated_at: meta.provider_session_updated_at || "",
197
+ });
198
+ }
199
+ }
200
+ return cache;
201
+ }
202
+
203
+ /**
204
+ * Execute probe: inject command and search for session ID
205
+ */
206
+ async function executeProbe({
207
+ projectRoot,
208
+ subscriberId,
209
+ agentType,
210
+ nickname,
211
+ attempts = 15,
212
+ intervalMs = 2000,
213
+ onResolved = null,
214
+ }) {
215
+ const marker = buildProbeMarker(nickname);
216
+
217
+ try {
218
+ const command = buildProbeCommand(agentType, nickname);
219
+ const bus = new EventBus(projectRoot);
220
+ bus.ensureBus();
221
+ await bus.inject(subscriberId, command);
222
+ } catch {
223
+ // ignore injection failures
224
+ }
225
+
226
+ const resolved = await resolveWithRetries(agentType, marker, attempts, intervalMs);
227
+ if (resolved && resolved.sessionId) {
228
+ persistProviderSession(projectRoot, subscriberId, resolved);
229
+ if (typeof onResolved === "function") {
230
+ onResolved(subscriberId, resolved);
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Schedule a provider session probe
237
+ *
238
+ * @param {Object} options
239
+ * @param {string} options.projectRoot - Project root directory
240
+ * @param {string} options.subscriberId - Subscriber ID (e.g., "claude-code:abc123")
241
+ * @param {string} options.agentType - Agent type ("claude-code" or "codex")
242
+ * @param {string} options.nickname - Agent nickname (e.g., "claude-47")
243
+ * @param {number} options.delayMs - Delay before injection
244
+ * @param {number} options.attempts - Number of search attempts
245
+ * @param {number} options.intervalMs - Interval between attempts
246
+ * @param {Function} options.onResolved - Callback when session ID is found
247
+ */
248
+ function scheduleProviderSessionProbe({
249
+ projectRoot,
250
+ subscriberId,
251
+ agentType,
252
+ nickname,
253
+ delayMs = 8000,
254
+ attempts = 15,
255
+ intervalMs = 2000,
256
+ onResolved = null,
257
+ }) {
258
+ if (!subscriberId || !agentType) return null;
259
+ if (agentType !== "codex" && agentType !== "claude-code") return null;
260
+ if (!nickname) return null;
261
+
262
+ const marker = buildProbeMarker(nickname);
263
+ persistProbeMarker(projectRoot, subscriberId, marker);
264
+
265
+ let executed = false;
266
+ let timer = null;
267
+
268
+ const execute = async () => {
269
+ if (executed) return;
270
+ executed = true;
271
+ if (timer) {
272
+ clearTimeout(timer);
273
+ timer = null;
274
+ }
275
+ await executeProbe({
276
+ projectRoot,
277
+ subscriberId,
278
+ agentType,
279
+ nickname,
280
+ attempts,
281
+ intervalMs,
282
+ onResolved,
283
+ });
284
+ };
285
+
286
+ // Schedule delayed execution (fallback)
287
+ timer = setTimeout(execute, delayMs);
288
+
289
+ // Return handle for early trigger
290
+ return {
291
+ subscriberId,
292
+ marker,
293
+ triggerNow: execute,
294
+ };
295
+ }
296
+
297
+ module.exports = {
298
+ scheduleProviderSessionProbe,
299
+ loadProviderSessionCache,
300
+ __private: {
301
+ buildProbeCommand,
302
+ recordContainsMarker,
303
+ containsProbeCommand,
304
+ escapeRegExp,
305
+ },
306
+ };
@@ -0,0 +1,90 @@
1
+ const fs = require("fs");
2
+ const { BUS_STATUS_PHASES } = require("../shared/eventContract");
3
+ const {
4
+ REPORT_PHASES,
5
+ normalizeReportInput,
6
+ appendReport,
7
+ updateReportState,
8
+ appendControllerInboxEntry,
9
+ } = require("../report/store");
10
+ const { getUfooPaths } = require("../ufoo/paths");
11
+
12
+ function resolveAgentDisplayName(projectRoot, agentId) {
13
+ if (!agentId) return "unknown-agent";
14
+ try {
15
+ const busPath = getUfooPaths(projectRoot).agentsFile;
16
+ const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
17
+ const meta = bus && bus.agents ? bus.agents[agentId] : null;
18
+ if (meta && typeof meta.nickname === "string" && meta.nickname.trim()) {
19
+ return meta.nickname.trim();
20
+ }
21
+ } catch {
22
+ // ignore
23
+ }
24
+ return agentId;
25
+ }
26
+
27
+ function toStatusPhase(reportPhase) {
28
+ if (reportPhase === REPORT_PHASES.START || reportPhase === REPORT_PHASES.PROGRESS) {
29
+ return BUS_STATUS_PHASES.START;
30
+ }
31
+ if (reportPhase === REPORT_PHASES.ERROR) return BUS_STATUS_PHASES.ERROR;
32
+ return BUS_STATUS_PHASES.DONE;
33
+ }
34
+
35
+ function formatStatusText(displayName, entry) {
36
+ if (entry.phase === REPORT_PHASES.START) {
37
+ const detail = entry.message || entry.summary || entry.task_id;
38
+ return `${displayName} ${detail}`;
39
+ }
40
+ if (entry.phase === REPORT_PHASES.PROGRESS) {
41
+ const detail = entry.message || entry.summary || entry.task_id;
42
+ return `${displayName} progress: ${detail}`;
43
+ }
44
+ if (entry.phase === REPORT_PHASES.ERROR) {
45
+ const detail = entry.error || entry.summary || entry.message || entry.task_id;
46
+ return `${displayName} failed: ${detail}`;
47
+ }
48
+ const detail = entry.summary || entry.message || entry.task_id;
49
+ return `${displayName} done: ${detail}`;
50
+ }
51
+
52
+ function buildReportStatus(entry, displayName) {
53
+ return {
54
+ phase: toStatusPhase(entry.phase),
55
+ key: `report:${entry.agent_id}:${entry.task_id}`,
56
+ text: formatStatusText(displayName, entry),
57
+ };
58
+ }
59
+
60
+ function publishToPrivateController(projectRoot, entry) {
61
+ if (!entry || !entry.controller_id) return;
62
+ appendControllerInboxEntry(projectRoot, entry.controller_id, entry);
63
+ }
64
+
65
+ async function recordAgentReport({
66
+ projectRoot,
67
+ report,
68
+ onStatus = () => {},
69
+ log = () => {},
70
+ }) {
71
+ const entry = normalizeReportInput(report);
72
+ appendReport(projectRoot, entry);
73
+ const state = updateReportState(projectRoot, entry);
74
+ publishToPrivateController(projectRoot, entry);
75
+ const displayName = resolveAgentDisplayName(projectRoot, entry.agent_id);
76
+ if (entry.scope !== "private") {
77
+ onStatus(buildReportStatus(entry, displayName));
78
+ }
79
+ log(`report ${entry.phase} scope=${entry.scope} agent=${entry.agent_id} task=${entry.task_id}`);
80
+ return { entry, state };
81
+ }
82
+
83
+ module.exports = {
84
+ recordAgentReport,
85
+ resolveAgentDisplayName,
86
+ toStatusPhase,
87
+ formatStatusText,
88
+ buildReportStatus,
89
+ publishToPrivateController,
90
+ };
package/src/daemon/run.js CHANGED
@@ -9,6 +9,8 @@ function runDaemonCli(argv) {
9
9
  const provider = process.env.UFOO_AGENT_PROVIDER || config.agentProvider || "codex-cli";
10
10
  const model =
11
11
  process.env.UFOO_AGENT_MODEL || config.agentModel || (provider === "claude-cli" ? "opus" : "");
12
+ const resumeMode = process.env.UFOO_FORCE_RESUME === "1" ? "force" : "auto";
13
+ const launchMode = config.launchMode || "terminal";
12
14
 
13
15
  if (cmd === "start" || cmd === "--start") {
14
16
  if (isRunning(projectRoot)) return;
@@ -23,13 +25,41 @@ function runDaemonCli(argv) {
23
25
  child.unref();
24
26
  return;
25
27
  }
26
- startDaemon({ projectRoot, provider, model });
28
+ startDaemon({ projectRoot, provider, model, resumeMode });
27
29
  return;
28
30
  }
29
31
  if (cmd === "stop" || cmd === "--stop") {
30
32
  stopDaemon(projectRoot);
31
33
  return;
32
34
  }
35
+ if (cmd === "restart" || cmd === "--restart") {
36
+ // Stop if running
37
+ if (isRunning(projectRoot)) {
38
+ stopDaemon(projectRoot);
39
+ // Wait for clean shutdown
40
+ let attempts = 0;
41
+ while (isRunning(projectRoot) && attempts < 50) {
42
+ attempts++;
43
+ require("child_process").spawnSync("sleep", ["0.1"]);
44
+ }
45
+ }
46
+ // Start fresh daemon
47
+ if (!process.env.UFOO_DAEMON_CHILD) {
48
+ const { spawn } = require("child_process");
49
+ const childEnv = { ...process.env, UFOO_DAEMON_CHILD: "1" };
50
+ const child = spawn(process.execPath, [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
51
+ detached: true,
52
+ stdio: "ignore",
53
+ env: childEnv,
54
+ cwd: projectRoot,
55
+ });
56
+ child.unref();
57
+ return;
58
+ }
59
+ // Manual restart does not auto-resume; crash-recovery is handled on next auto start with stale lock detection.
60
+ startDaemon({ projectRoot, provider, model, resumeMode: "none" });
61
+ return;
62
+ }
33
63
  if (cmd === "status" || cmd === "--status") {
34
64
  const running = isRunning(projectRoot);
35
65
  // eslint-disable-next-line no-console
@@ -1,8 +1,11 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { getUfooPaths } = require("../ufoo/paths");
4
+ const { isMetaActive } = require("../bus/utils");
5
+ const { readReportSummary } = require("../report/store");
3
6
 
4
7
  function readBus(projectRoot) {
5
- const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
8
+ const busPath = getUfooPaths(projectRoot).agentsFile;
6
9
  try {
7
10
  return JSON.parse(fs.readFileSync(busPath, "utf8"));
8
11
  } catch {
@@ -11,7 +14,9 @@ function readBus(projectRoot) {
11
14
  }
12
15
 
13
16
  function readDecisions(projectRoot) {
14
- const dir = path.join(projectRoot, ".ufoo", "context", "DECISIONS");
17
+ const DecisionsManager = require("../context/decisions");
18
+ const manager = new DecisionsManager(projectRoot);
19
+ const dir = manager.decisionsDir;
15
20
  let open = 0;
16
21
  try {
17
22
  const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
@@ -28,7 +33,7 @@ function readDecisions(projectRoot) {
28
33
  }
29
34
 
30
35
  function readUnread(projectRoot) {
31
- const queuesDir = path.join(projectRoot, ".ufoo", "bus", "queues");
36
+ const queuesDir = getUfooPaths(projectRoot).busQueuesDir;
32
37
  let total = 0;
33
38
  const perSubscriber = {};
34
39
  try {
@@ -48,21 +53,51 @@ function readUnread(projectRoot) {
48
53
  return { total, perSubscriber };
49
54
  }
50
55
 
51
- function buildStatus(projectRoot) {
56
+ function isHiddenSubscriber(id, meta) {
57
+ if (!id) return false;
58
+ if (id === "ufoo-agent") return true;
59
+ if (meta && meta.nickname === "ufoo-agent") return true;
60
+ if (meta && meta.agent_type === "ufoo-agent") return true;
61
+ return false;
62
+ }
63
+
64
+ function normalizeCronTasks(raw = []) {
65
+ const items = Array.isArray(raw) ? raw : [];
66
+ return items.map((task) => ({
67
+ id: String(task && task.id ? task.id : ""),
68
+ intervalMs: Number(task && task.intervalMs ? task.intervalMs : 0) || 0,
69
+ interval: String(task && task.interval ? task.interval : ""),
70
+ targets: Array.isArray(task && task.targets) ? task.targets.slice() : [],
71
+ prompt: String(task && task.prompt ? task.prompt : ""),
72
+ summary: String(task && task.summary ? task.summary : ""),
73
+ createdAt: Number(task && task.createdAt ? task.createdAt : 0) || 0,
74
+ lastRunAt: Number(task && task.lastRunAt ? task.lastRunAt : 0) || 0,
75
+ tickCount: Number(task && task.tickCount ? task.tickCount : 0) || 0,
76
+ }));
77
+ }
78
+
79
+ function buildStatus(projectRoot, options = {}) {
52
80
  const bus = readBus(projectRoot);
53
81
  const decisions = readDecisions(projectRoot);
54
82
  const unread = readUnread(projectRoot);
55
- const subscribers = bus ? Object.keys(bus.subscribers || {}) : [];
83
+ const reports = readReportSummary(projectRoot);
84
+ const subscribers = bus ? Object.keys(bus.agents || {}) : [];
85
+ const cronTasks = normalizeCronTasks(options.cronTasks || []);
86
+
56
87
  const activeEntries = bus
57
- ? Object.entries(bus.subscribers || {})
58
- .filter(([, meta]) => meta.status === "active")
88
+ ? Object.entries(bus.agents || {})
89
+ .filter(([, meta]) => isMetaActive(meta))
90
+ .filter(([id, meta]) => !isHiddenSubscriber(id, meta))
59
91
  .map(([id, meta]) => ({ id, meta }))
60
92
  : [];
61
93
  const active = activeEntries.map(({ id }) => id);
62
94
  const activeMeta = activeEntries.map(({ id, meta }) => {
63
95
  const nickname = meta?.nickname || "";
64
96
  const display = nickname ? nickname : id;
65
- return { id, nickname, display };
97
+ const launch_mode = meta?.launch_mode || "unknown";
98
+ const tmux_pane = meta?.tmux_pane || "";
99
+ const tty = meta?.tty || "";
100
+ return { id, nickname, display, launch_mode, tmux_pane, tty };
66
101
  });
67
102
 
68
103
  return {
@@ -72,6 +107,11 @@ function buildStatus(projectRoot) {
72
107
  active_meta: activeMeta,
73
108
  unread,
74
109
  decisions,
110
+ reports,
111
+ cron: {
112
+ count: cronTasks.length,
113
+ tasks: cronTasks,
114
+ },
75
115
  };
76
116
  }
77
117
 
@@ -0,0 +1,50 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const ContextDoctor = require("../context/doctor");
4
+
5
+ class RepoDoctor {
6
+ constructor(repoRoot) {
7
+ this.repoRoot = repoRoot;
8
+ this.failed = false;
9
+ }
10
+
11
+ fail(message) {
12
+ console.error(`FAIL: ${message}`);
13
+ this.failed = true;
14
+ }
15
+
16
+ run() {
17
+ const contextMod = path.join(this.repoRoot, "modules", "context");
18
+
19
+ const contextExists = fs.existsSync(contextMod);
20
+ if (!contextExists) {
21
+ this.fail(`missing ${contextMod}`);
22
+ }
23
+
24
+ if (contextExists) {
25
+ const contextDoctor = new ContextDoctor(this.repoRoot);
26
+ const ok = contextDoctor.lintProtocol();
27
+ if (!ok) this.failed = true;
28
+ }
29
+
30
+ console.log("=== ufoo doctor ===");
31
+ console.log(`Monorepo: ${this.repoRoot}`);
32
+ console.log("Modules:");
33
+ if (contextExists) {
34
+ console.log(`- context: ${contextMod}`);
35
+ }
36
+ const resources = path.join(this.repoRoot, "modules", "resources");
37
+ if (fs.existsSync(resources)) {
38
+ console.log(`- resources: ${resources}`);
39
+ }
40
+
41
+ if (this.failed) {
42
+ console.log("Status: FAILED");
43
+ return false;
44
+ }
45
+ console.log("Status: OK");
46
+ return true;
47
+ }
48
+ }
49
+
50
+ module.exports = RepoDoctor;