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,131 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ subscriberToSafeName,
5
+ ensureDir,
6
+ readJSONL,
7
+ appendJSONL,
8
+ truncateFile,
9
+ } = require("./utils");
10
+
11
+ /**
12
+ * 队列管理器
13
+ */
14
+ class QueueManager {
15
+ constructor(busDir) {
16
+ this.busDir = busDir;
17
+ this.queuesDir = path.join(busDir, "queues");
18
+ }
19
+
20
+ /**
21
+ * 获取订阅者的队列目录
22
+ */
23
+ getQueueDir(subscriber) {
24
+ const safeName = subscriberToSafeName(subscriber);
25
+ return path.join(this.queuesDir, safeName);
26
+ }
27
+
28
+ /**
29
+ * 确保队列目录存在
30
+ */
31
+ ensureQueueDir(subscriber) {
32
+ const queueDir = this.getQueueDir(subscriber);
33
+ ensureDir(queueDir);
34
+ return queueDir;
35
+ }
36
+
37
+ /**
38
+ * 获取 offset 文件路径
39
+ */
40
+ getOffsetPath(subscriber) {
41
+ return path.join(this.queuesDir, `${subscriberToSafeName(subscriber)}.offset`);
42
+ }
43
+
44
+ /**
45
+ * 获取 pending 文件路径
46
+ */
47
+ getPendingPath(subscriber) {
48
+ return path.join(this.getQueueDir(subscriber), "pending.jsonl");
49
+ }
50
+
51
+ /**
52
+ * 获取 tty 文件路径
53
+ */
54
+ getTtyPath(subscriber) {
55
+ return path.join(this.getQueueDir(subscriber), "tty");
56
+ }
57
+
58
+ /**
59
+ * 读取 offset
60
+ */
61
+ async getOffset(subscriber) {
62
+ const offsetPath = this.getOffsetPath(subscriber);
63
+ if (!fs.existsSync(offsetPath)) {
64
+ return 0;
65
+ }
66
+ const content = fs.readFileSync(offsetPath, "utf8").trim();
67
+ return parseInt(content, 10) || 0;
68
+ }
69
+
70
+ /**
71
+ * 设置 offset
72
+ */
73
+ async setOffset(subscriber, seq) {
74
+ const offsetPath = this.getOffsetPath(subscriber);
75
+ ensureDir(path.dirname(offsetPath));
76
+ fs.writeFileSync(offsetPath, `${seq}\n`, "utf8");
77
+ }
78
+
79
+ /**
80
+ * 读取待处理消息
81
+ */
82
+ async readPending(subscriber) {
83
+ const pendingPath = this.getPendingPath(subscriber);
84
+ return readJSONL(pendingPath);
85
+ }
86
+
87
+ /**
88
+ * 追加待处理消息
89
+ */
90
+ async appendPending(subscriber, event) {
91
+ this.ensureQueueDir(subscriber);
92
+ const pendingPath = this.getPendingPath(subscriber);
93
+ appendJSONL(pendingPath, event);
94
+ }
95
+
96
+ /**
97
+ * 清空待处理消息
98
+ */
99
+ async clearPending(subscriber) {
100
+ const pendingPath = this.getPendingPath(subscriber);
101
+ truncateFile(pendingPath);
102
+ }
103
+
104
+ /**
105
+ * 检查是否有待处理消息
106
+ */
107
+ async hasPending(subscriber) {
108
+ const pending = await this.readPending(subscriber);
109
+ return pending.length > 0;
110
+ }
111
+
112
+ /**
113
+ * 保存 tty 设备路径
114
+ */
115
+ async saveTty(subscriber, tty) {
116
+ this.ensureQueueDir(subscriber);
117
+ const ttyPath = this.getTtyPath(subscriber);
118
+ fs.writeFileSync(ttyPath, tty, "utf8");
119
+ }
120
+
121
+ /**
122
+ * 读取 tty 设备路径
123
+ */
124
+ async readTty(subscriber) {
125
+ const ttyPath = this.getTtyPath(subscriber);
126
+ if (!fs.existsSync(ttyPath)) return null;
127
+ return fs.readFileSync(ttyPath, "utf8").trim();
128
+ }
129
+ }
130
+
131
+ module.exports = QueueManager;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Terminal notification - sends visual alert to a terminal by TTY path.
3
+ *
4
+ * Supports:
5
+ * - iTerm2: OSC 9 notification (native macOS notification)
6
+ * - All terminals: terminal bell (\x07)
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const { isITerm2 } = require("../terminal/detect");
11
+
12
+ function shakeTerminalByTty(ttyPath, options = {}) {
13
+ if (!ttyPath) return false;
14
+
15
+ try {
16
+ const fd = fs.openSync(ttyPath, "w");
17
+ // Terminal bell works universally
18
+ fs.writeSync(fd, "\x07");
19
+ fs.closeSync(fd);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ module.exports = { shakeTerminalByTty };
@@ -0,0 +1,296 @@
1
+ const fs = require("fs");
2
+ const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessInfo } = require("./utils");
3
+ const NicknameManager = require("./nickname");
4
+ const { spawnSync } = require("child_process");
5
+
6
+ /**
7
+ * 获取当前终端的 tty 路径
8
+ */
9
+ function resolveTtyFromPath(fdPath) {
10
+ try {
11
+ const real = fs.realpathSync(fdPath);
12
+ if (real && real.startsWith("/dev/")) {
13
+ return real;
14
+ }
15
+ } catch {
16
+ // ignore
17
+ }
18
+ return "";
19
+ }
20
+
21
+ function normalizeTty(ttyPath) {
22
+ if (!ttyPath) return "";
23
+ const trimmed = String(ttyPath).trim();
24
+ if (!trimmed || trimmed === "not a tty") return "";
25
+ if (trimmed === "/dev/tty") return "";
26
+ return trimmed;
27
+ }
28
+
29
+ function tryTtyWithStdin(fd) {
30
+ try {
31
+ const res = spawnSync("tty", {
32
+ stdio: [fd, "pipe", "ignore"],
33
+ encoding: "utf8",
34
+ });
35
+ if (res && res.status === 0) {
36
+ const out = normalizeTty(res.stdout || "");
37
+ if (out) return out;
38
+ }
39
+ } catch {
40
+ // ignore
41
+ }
42
+ return "";
43
+ }
44
+
45
+ function getTtyPath() {
46
+ // 1) Try stdin directly (inherits real tty if present)
47
+ let ttyPath = tryTtyWithStdin(0);
48
+ if (ttyPath) return ttyPath;
49
+
50
+ // 2) Try controlling tty explicitly (works even if stdin is detached)
51
+ try {
52
+ const fd = fs.openSync("/dev/tty", "r");
53
+ ttyPath = tryTtyWithStdin(fd);
54
+ fs.closeSync(fd);
55
+ if (ttyPath) return ttyPath;
56
+ } catch {
57
+ // ignore
58
+ }
59
+
60
+ // 3) Fallback to stdout/stderr device paths
61
+ if (process.stdout.isTTY) {
62
+ ttyPath = normalizeTty(resolveTtyFromPath("/dev/stdout"));
63
+ if (ttyPath) return ttyPath;
64
+ }
65
+ if (process.stderr.isTTY) {
66
+ ttyPath = normalizeTty(resolveTtyFromPath("/dev/stderr"));
67
+ if (ttyPath) return ttyPath;
68
+ }
69
+
70
+ // Final fallback to controlling tty path (may be /dev/tty)
71
+ return normalizeTty(resolveTtyFromPath("/dev/tty"));
72
+ }
73
+
74
+ function getJoinedPid() {
75
+ const raw = process.env.UFOO_PARENT_PID || "";
76
+ const parsed = parseInt(raw, 10);
77
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
78
+ return process.pid;
79
+ }
80
+
81
+ /**
82
+ * 订阅者管理
83
+ */
84
+ class SubscriberManager {
85
+ constructor(busData, queueManager) {
86
+ this.busData = busData;
87
+ this.queueManager = queueManager;
88
+ }
89
+
90
+ async cleanupDuplicateTty(currentSubscriber, ttyPath) {
91
+ if (!ttyPath) return;
92
+ if (!this.busData.agents) return;
93
+
94
+ const entries = Object.entries(this.busData.agents);
95
+ for (const [id, meta] of entries) {
96
+ if (id === currentSubscriber) continue;
97
+ const metaTtyRaw = meta?.tty || "";
98
+ const metaTty = isValidTty(metaTtyRaw)
99
+ ? metaTtyRaw
100
+ : (await this.queueManager.readTty(id));
101
+ if (!metaTty) continue;
102
+ if (metaTty === ttyPath) {
103
+ // Remove stale subscriber using same tty
104
+ delete this.busData.agents[id];
105
+ try {
106
+ const queueDir = this.queueManager.getQueueDir(id);
107
+ if (queueDir) {
108
+ fs.rmSync(queueDir, { recursive: true, force: true });
109
+ }
110
+ const offsetPath = this.queueManager.getOffsetPath(id);
111
+ if (offsetPath) fs.rmSync(offsetPath, { force: true });
112
+ } catch {
113
+ // ignore cleanup errors
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 加入总线
121
+ */
122
+ async join(sessionId, agentType, nickname = null, options = {}) {
123
+ // Special case: ufoo-agent uses fixed ID without suffix
124
+ const subscriber = (sessionId === "ufoo-agent")
125
+ ? "ufoo-agent"
126
+ : `${agentType}:${sessionId}`;
127
+
128
+ if (!this.busData.agents) {
129
+ this.busData.agents = {};
130
+ }
131
+
132
+ const nicknameManager = new NicknameManager(this.busData);
133
+
134
+ // 检查是否是重新加入(rejoin)
135
+ const existingMeta = this.busData.agents[subscriber];
136
+ let finalNickname = nickname;
137
+
138
+ if (existingMeta && existingMeta.nickname) {
139
+ // 重新加入,保留原昵称
140
+ finalNickname = existingMeta.nickname;
141
+ } else if (nickname) {
142
+ // 新昵称,检查冲突
143
+ if (nicknameManager.nicknameExists(nickname, subscriber)) {
144
+ throw new Error(`Nickname "${nickname}" already exists`);
145
+ }
146
+ finalNickname = nickname;
147
+ } else {
148
+ // 自动生成昵称
149
+ finalNickname = nicknameManager.generateAutoNickname(agentType);
150
+ }
151
+
152
+ const launchMode = options.launchMode || process.env.UFOO_LAUNCH_MODE || "";
153
+ const overridePid = Number.isFinite(options.parentPid) && options.parentPid > 0
154
+ ? options.parentPid
155
+ : null;
156
+ const hasOverrideTty = Object.prototype.hasOwnProperty.call(options, "tty");
157
+ const overrideTty = (typeof options.tty === "string" && isValidTty(options.tty.trim()))
158
+ ? options.tty.trim()
159
+ : "";
160
+ const detectedTty = hasOverrideTty ? overrideTty : getTtyPath();
161
+ const tty = overrideTty || (isValidTty(detectedTty) ? detectedTty : "");
162
+ const preservedTty = !tty && launchMode !== "internal" && isValidTty(existingMeta?.tty)
163
+ ? existingMeta.tty
164
+ : "";
165
+ const finalTty = tty || preservedTty;
166
+ const ttyInfo = finalTty ? getTtyProcessInfo(finalTty) : null;
167
+
168
+ // 清理同一 tty 的旧订阅者(避免重复启动污染)
169
+ await this.cleanupDuplicateTty(subscriber, finalTty);
170
+
171
+ // 更新订阅者信息(保留已有字段,如 provider_session_*)
172
+ const preserved = existingMeta && typeof existingMeta === "object"
173
+ ? { ...existingMeta }
174
+ : {};
175
+ this.busData.agents[subscriber] = {
176
+ ...preserved,
177
+ agent_type: agentType,
178
+ nickname: finalNickname,
179
+ status: "active",
180
+ joined_at: existingMeta?.joined_at || getTimestamp(),
181
+ last_seen: getTimestamp(),
182
+ pid: overridePid || getJoinedPid(),
183
+ tty: finalTty,
184
+ tty_shell_pid: ttyInfo?.shellPid || 0,
185
+ tmux_pane: options.tmuxPane || process.env.TMUX_PANE || "",
186
+ launch_mode: launchMode,
187
+ };
188
+
189
+ // 如果传入了 providerSessionId(从旧 session 恢复),设置它
190
+ if (options.providerSessionId) {
191
+ this.busData.agents[subscriber].provider_session_id = options.providerSessionId;
192
+ }
193
+
194
+ // 保存 tty 信息
195
+ if (this.busData.agents[subscriber].tty) {
196
+ await this.queueManager.saveTty(
197
+ subscriber,
198
+ this.busData.agents[subscriber].tty
199
+ );
200
+ } else {
201
+ // 清理旧 tty 文件,避免错误注入
202
+ try {
203
+ const ttyPath = this.queueManager.getTtyPath(subscriber);
204
+ if (ttyPath && fs.existsSync(ttyPath)) {
205
+ fs.rmSync(ttyPath, { force: true });
206
+ }
207
+ } catch {
208
+ // ignore
209
+ }
210
+ }
211
+
212
+ // 创建队列目录
213
+ this.queueManager.ensureQueueDir(subscriber);
214
+
215
+ return { subscriber, nickname: finalNickname };
216
+ }
217
+
218
+ /**
219
+ * 离开总线
220
+ */
221
+ async leave(subscriber) {
222
+ if (!this.busData.agents || !this.busData.agents[subscriber]) {
223
+ return false;
224
+ }
225
+
226
+ this.busData.agents[subscriber].status = "inactive";
227
+ this.busData.agents[subscriber].last_seen = getTimestamp();
228
+
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * 重命名订阅者
234
+ */
235
+ async rename(subscriber, newNickname) {
236
+ if (!this.busData.agents || !this.busData.agents[subscriber]) {
237
+ throw new Error(`Subscriber "${subscriber}" not found`);
238
+ }
239
+
240
+ const nicknameManager = new NicknameManager(this.busData);
241
+
242
+ // 检查昵称冲突
243
+ if (nicknameManager.nicknameExists(newNickname, subscriber)) {
244
+ throw new Error(`Nickname "${newNickname}" already exists`);
245
+ }
246
+
247
+ const oldNickname = this.busData.agents[subscriber].nickname;
248
+ this.busData.agents[subscriber].nickname = newNickname;
249
+
250
+ return { subscriber, oldNickname, newNickname };
251
+ }
252
+
253
+ /**
254
+ * 获取所有在线订阅者
255
+ */
256
+ getActiveSubscribers() {
257
+ if (!this.busData.agents) return [];
258
+
259
+ return Object.entries(this.busData.agents)
260
+ .filter(([, meta]) => isMetaActive(meta))
261
+ .map(([id, meta]) => ({ id, ...meta }));
262
+ }
263
+
264
+ /**
265
+ * 获取订阅者信息
266
+ */
267
+ getSubscriber(subscriber) {
268
+ return this.busData.agents?.[subscriber] || null;
269
+ }
270
+
271
+ /**
272
+ * 更新订阅者的最后活动时间
273
+ */
274
+ updateLastSeen(subscriber) {
275
+ if (this.busData.agents && this.busData.agents[subscriber]) {
276
+ this.busData.agents[subscriber].last_seen = getTimestamp();
277
+ }
278
+ }
279
+
280
+ /**
281
+ * 清理不活跃的订阅者
282
+ */
283
+ cleanupInactive() {
284
+ if (!this.busData.agents) return;
285
+
286
+ for (const [id, meta] of Object.entries(this.busData.agents)) {
287
+ if (meta.status !== "active") continue;
288
+ // PID 已死则直接标记 inactive(不依赖 tty 检测,因为 tty 可能被新 agent 复用)
289
+ if (meta.pid && !isAgentPidAlive(meta.pid)) {
290
+ meta.status = "inactive";
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ module.exports = SubscriberManager;