u-foo 1.0.3 → 1.0.6

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 (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
@@ -0,0 +1,357 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { spawnSync } = require("child_process");
5
+
6
+ /**
7
+ * 获取当前 UTC 时间戳(ISO 8601 格式)
8
+ */
9
+ function getTimestamp() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ /**
14
+ * 获取当前日期(YYYY-MM-DD 格式)
15
+ */
16
+ function getDate() {
17
+ return new Date().toISOString().split("T")[0];
18
+ }
19
+
20
+ /**
21
+ * 生成实例 ID(8 位十六进制)
22
+ */
23
+ function generateInstanceId() {
24
+ return crypto.randomBytes(4).toString("hex");
25
+ }
26
+
27
+ /**
28
+ * 将订阅者 ID 转换为安全的文件名
29
+ * 例如:claude-code:abc123 -> claude-code_abc123
30
+ */
31
+ function subscriberToSafeName(subscriber) {
32
+ return subscriber.replace(/:/g, "_");
33
+ }
34
+
35
+ /**
36
+ * 将安全文件名转换回订阅者 ID
37
+ * 例如:claude-code_abc123 -> claude-code:abc123
38
+ */
39
+ function safeNameToSubscriber(safeName) {
40
+ // 只替换第一个下划线为冒号
41
+ const match = safeName.match(/^([^_]+)_(.+)$/);
42
+ if (match) {
43
+ return `${match[1]}:${match[2]}`;
44
+ }
45
+ return safeName;
46
+ }
47
+
48
+ /**
49
+ * 检查进程是否存活
50
+ */
51
+ function isPidAlive(pid) {
52
+ if (!pid || pid === 0) return false;
53
+ try {
54
+ process.kill(pid, 0);
55
+ return true;
56
+ } catch (err) {
57
+ // Sandbox can return EPERM for other processes even if they are alive.
58
+ if (err && err.code === "EPERM") return true;
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 获取进程命令名(用于校验 PID 是否仍属于 agent 进程)
65
+ */
66
+ function getPidCommand(pid) {
67
+ if (!pid || pid === 0) return "";
68
+ try {
69
+ const res = spawnSync("ps", ["-p", String(pid), "-o", "comm="], {
70
+ encoding: "utf8",
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ });
73
+ if (res.status === 0) {
74
+ return (res.stdout || "").trim();
75
+ }
76
+ } catch {
77
+ // ignore
78
+ }
79
+ return "";
80
+ }
81
+
82
+ /**
83
+ * 判断 PID 是否为可识别的 agent 进程
84
+ */
85
+ function isAgentPidAlive(pid) {
86
+ if (!isPidAlive(pid)) return false;
87
+ const cmd = getPidCommand(pid);
88
+ if (!cmd) return true;
89
+ return /(claude|codex|node)/i.test(cmd);
90
+ }
91
+
92
+ /**
93
+ * 检查 tty 路径是否有效(用于注入)
94
+ */
95
+ function isValidTty(ttyPath) {
96
+ if (!ttyPath) return false;
97
+ if (ttyPath === "/dev/tty") return false;
98
+ if (!ttyPath.startsWith("/dev/")) return false;
99
+ return true;
100
+ }
101
+
102
+ function normalizeTty(ttyPath) {
103
+ if (!ttyPath) return "";
104
+ const trimmed = String(ttyPath).trim();
105
+ if (!trimmed || trimmed === "not a tty") return "";
106
+ if (trimmed === "/dev/tty") return "";
107
+ return trimmed;
108
+ }
109
+
110
+ function getCurrentTty() {
111
+ try {
112
+ const res = spawnSync("tty", {
113
+ stdio: [0, "pipe", "ignore"],
114
+ encoding: "utf8",
115
+ });
116
+ if (res && res.status === 0) {
117
+ const tty = normalizeTty(res.stdout || "");
118
+ return isValidTty(tty) ? tty : "";
119
+ }
120
+ } catch {
121
+ // ignore
122
+ }
123
+ return "";
124
+ }
125
+
126
+ function parsePsLines(raw) {
127
+ const lines = String(raw || "").trim().split(/\r?\n/).filter(Boolean);
128
+ return lines.map((line) => {
129
+ const match = line.trim().match(/^(\d+)\s+(.+)$/);
130
+ if (!match) return null;
131
+ return { pid: parseInt(match[1], 10), comm: match[2].trim() };
132
+ }).filter(Boolean);
133
+ }
134
+
135
+ function getTtyProcessInfo(ttyPath) {
136
+ if (!isValidTty(ttyPath)) {
137
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
138
+ }
139
+ const ttyName = ttyPath.replace("/dev/", "");
140
+ try {
141
+ const res = spawnSync("ps", ["-t", ttyName, "-o", "pid=,comm="], {
142
+ encoding: "utf8",
143
+ stdio: ["ignore", "pipe", "ignore"],
144
+ });
145
+ if (res.status !== 0) {
146
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
147
+ }
148
+ const processes = parsePsLines(res.stdout || "");
149
+ if (processes.length === 0) {
150
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
151
+ }
152
+ const shellNames = new Set(["zsh", "bash", "fish", "sh", "login"]);
153
+ const hasAgent = processes.some((p) => /(codex|claude|node)/i.test(p.comm));
154
+ const nonShell = processes.filter((p) => !shellNames.has(p.comm));
155
+ const idle = !hasAgent && nonShell.length === 0;
156
+ const shell = processes.find((p) => shellNames.has(p.comm));
157
+ return { alive: true, idle, hasAgent, shellPid: shell ? shell.pid : 0, processes };
158
+ } catch {
159
+ return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * 确保目录存在
165
+ */
166
+ function ensureDir(dir) {
167
+ if (!fs.existsSync(dir)) {
168
+ fs.mkdirSync(dir, { recursive: true });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 原子性地追加内容到文件
174
+ * @param {string} filePath - 文件路径
175
+ * @param {string} content - 要追加的内容
176
+ */
177
+ function appendFileAtomic(filePath, content) {
178
+ ensureDir(path.dirname(filePath));
179
+ fs.appendFileSync(filePath, content, { encoding: "utf8", flag: "a" });
180
+ }
181
+
182
+ /**
183
+ * 原子性地写入文件
184
+ */
185
+ function writeFileAtomic(filePath, content) {
186
+ ensureDir(path.dirname(filePath));
187
+ const tmpFile = `${filePath}.tmp.${Date.now()}`;
188
+ fs.writeFileSync(tmpFile, content, "utf8");
189
+ fs.renameSync(tmpFile, filePath);
190
+ }
191
+
192
+ /**
193
+ * 读取 JSON 文件
194
+ */
195
+ function readJSON(filePath, defaultValue = null) {
196
+ try {
197
+ if (!fs.existsSync(filePath)) return defaultValue;
198
+ const content = fs.readFileSync(filePath, "utf8");
199
+ return JSON.parse(content);
200
+ } catch (err) {
201
+ return defaultValue;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * 写入 JSON 文件(格式化)
207
+ */
208
+ function writeJSON(filePath, data) {
209
+ const content = JSON.stringify(data, null, 2);
210
+ writeFileAtomic(filePath, content);
211
+ }
212
+
213
+ /**
214
+ * 读取 JSONL 文件的所有行
215
+ */
216
+ function readJSONL(filePath) {
217
+ if (!fs.existsSync(filePath)) return [];
218
+ const content = fs.readFileSync(filePath, "utf8").trim();
219
+ if (!content) return [];
220
+ return content.split("\n").map((line) => {
221
+ try {
222
+ return JSON.parse(line);
223
+ } catch {
224
+ return null;
225
+ }
226
+ }).filter(Boolean);
227
+ }
228
+
229
+ /**
230
+ * 追加一行到 JSONL 文件
231
+ */
232
+ function appendJSONL(filePath, data) {
233
+ const line = JSON.stringify(data);
234
+ appendFileAtomic(filePath, `${line}\n`);
235
+ }
236
+
237
+ /**
238
+ * 读取文件的最后一行
239
+ */
240
+ function readLastLine(filePath) {
241
+ if (!fs.existsSync(filePath)) return null;
242
+ const content = fs.readFileSync(filePath, "utf8").trim();
243
+ if (!content) return null;
244
+ const lines = content.split("\n");
245
+ return lines[lines.length - 1];
246
+ }
247
+
248
+ /**
249
+ * 清空文件内容
250
+ */
251
+ function truncateFile(filePath) {
252
+ if (fs.existsSync(filePath)) {
253
+ fs.writeFileSync(filePath, "", "utf8");
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 日志输出(带颜色)
259
+ */
260
+ const colors = {
261
+ reset: "\x1b[0m",
262
+ red: "\x1b[0;31m",
263
+ green: "\x1b[0;32m",
264
+ yellow: "\x1b[0;33m",
265
+ blue: "\x1b[0;34m",
266
+ cyan: "\x1b[0;36m",
267
+ };
268
+
269
+ function logInfo(message) {
270
+ console.log(`${colors.blue}[bus]${colors.reset} ${message}`);
271
+ }
272
+
273
+ function logOk(message) {
274
+ console.log(`${colors.green}[bus]${colors.reset} ${message}`);
275
+ }
276
+
277
+ function logWarn(message) {
278
+ console.log(`${colors.yellow}[bus]${colors.reset} ${message}`);
279
+ }
280
+
281
+ function logError(message) {
282
+ console.error(`${colors.red}[bus]${colors.reset} ${message}`);
283
+ }
284
+
285
+ /**
286
+ * 统一的 agent 存活检测逻辑
287
+ * buildStatus() 和 getActiveSubscribers() 共用此函数
288
+ *
289
+ * 检测顺序:
290
+ * 1. status=inactive → 离线
291
+ * 2. PID 存活 → 在线
292
+ * 3. TTY 上有 agent 进程 → 在线
293
+ * 4. PID 存在但 dead(且 TTY 也没有 agent)→ 离线
294
+ * 5. 无 PID 时,last_seen 在 30s 内 → 在线(心跳兜底)
295
+ * 6. 其余 → 离线
296
+ */
297
+ const HEARTBEAT_TIMEOUT_MS = 30 * 1000; // notifier 每 2s 心跳,15 次余量
298
+
299
+ function isMetaActive(meta) {
300
+ if (!meta) return false;
301
+
302
+ // 1. 显式标记为 inactive
303
+ if (meta.status === "inactive") return false;
304
+
305
+ // 2. PID 存活(最可靠)
306
+ if (meta.pid && isAgentPidAlive(meta.pid)) return true;
307
+
308
+ // 3. TTY 上有 agent 进程
309
+ if (meta.tty) {
310
+ const ttyInfo = getTtyProcessInfo(meta.tty);
311
+ if (ttyInfo && ttyInfo.hasAgent) return true;
312
+ }
313
+
314
+ // 4. PID 存在但 dead(且 TTY 也没有 agent)→ 离线
315
+ if (meta.pid) return false;
316
+
317
+ // 5. 无 PID,用 last_seen 心跳超时兜底
318
+ if (meta.status === "active" && meta.last_seen) {
319
+ const age = Date.now() - new Date(meta.last_seen).getTime();
320
+ return age <= HEARTBEAT_TIMEOUT_MS;
321
+ }
322
+
323
+ // 6. status=active 但无任何信息
324
+ if (meta.status === "active") return true;
325
+
326
+ return false;
327
+ }
328
+
329
+ module.exports = {
330
+ getTimestamp,
331
+ getDate,
332
+ generateInstanceId,
333
+ subscriberToSafeName,
334
+ safeNameToSubscriber,
335
+ isPidAlive,
336
+ getPidCommand,
337
+ isAgentPidAlive,
338
+ isMetaActive,
339
+ HEARTBEAT_TIMEOUT_MS,
340
+ isValidTty,
341
+ getCurrentTty,
342
+ getTtyProcessInfo,
343
+ ensureDir,
344
+ appendFileAtomic,
345
+ writeFileAtomic,
346
+ readJSON,
347
+ writeJSON,
348
+ readJSONL,
349
+ appendJSONL,
350
+ readLastLine,
351
+ truncateFile,
352
+ logInfo,
353
+ logOk,
354
+ logWarn,
355
+ logError,
356
+ colors,
357
+ };