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.
- package/README.md +67 -8
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +117 -0
- package/SKILLS/uinit/SKILL.md +73 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo.js +13 -0
- package/modules/AGENTS.template.md +15 -7
- package/modules/bus/README.md +28 -23
- package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +61 -1
- package/package.json +16 -4
- package/scripts/.archived/bash-to-js-migration/README.md +46 -0
- package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
- package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
- package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
- package/scripts/banner.sh +2 -89
- package/scripts/postinstall.js +59 -0
- package/src/agent/cliRunner.js +33 -5
- package/src/agent/internalRunner.js +78 -51
- package/src/agent/launcher.js +702 -0
- package/src/agent/notifier.js +200 -0
- package/src/agent/ptyRunner.js +377 -0
- package/src/agent/ptyWrapper.js +354 -0
- package/src/agent/readyDetector.js +159 -0
- package/src/agent/ufooAgent.js +37 -42
- package/src/bus/API_DESIGN.md +204 -0
- package/src/bus/activate.js +156 -0
- package/src/bus/daemon.js +308 -0
- package/src/bus/index.js +785 -0
- package/src/bus/inject.js +285 -0
- package/src/bus/message.js +302 -0
- package/src/bus/nickname.js +86 -0
- package/src/bus/queue.js +131 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/subscriber.js +296 -0
- package/src/bus/utils.js +357 -0
- package/src/chat/index.js +1842 -249
- package/src/cli.js +658 -95
- package/src/config.js +9 -2
- package/src/context/decisions.js +314 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +38 -0
- package/src/daemon/index.js +749 -94
- package/src/daemon/ops.js +395 -51
- package/src/daemon/providerSessions.js +291 -0
- package/src/daemon/run.js +34 -1
- package/src/daemon/status.js +24 -7
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +264 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +252 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +41 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +73 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
- /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
- /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
- /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
- /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
- /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
- /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
- /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
- /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
- /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
- /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ufoo 初始化管理
|
|
6
|
+
*/
|
|
7
|
+
class UfooInit {
|
|
8
|
+
constructor(repoRoot) {
|
|
9
|
+
this.repoRoot = repoRoot;
|
|
10
|
+
this.contextMod = path.join(repoRoot, "modules", "context");
|
|
11
|
+
this.busMod = path.join(repoRoot, "modules", "bus");
|
|
12
|
+
this.resourcesMod = path.join(repoRoot, "modules", "resources");
|
|
13
|
+
this.agentsTemplate = path.join(repoRoot, "modules", "AGENTS.template.md");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 初始化项目
|
|
18
|
+
*/
|
|
19
|
+
async init(options = {}) {
|
|
20
|
+
const modules = (options.modules || "context").split(",");
|
|
21
|
+
const project = options.project || process.cwd();
|
|
22
|
+
|
|
23
|
+
console.log("=== ufoo init ===");
|
|
24
|
+
console.log(`Project directory: ${project}`);
|
|
25
|
+
console.log(`Modules: ${modules.join(", ")}`);
|
|
26
|
+
console.log();
|
|
27
|
+
|
|
28
|
+
// 确保 AGENTS.md 和 CLAUDE.md 存在
|
|
29
|
+
this.ensureAgentsFiles(project);
|
|
30
|
+
|
|
31
|
+
// 初始化核心
|
|
32
|
+
this.initCore(project);
|
|
33
|
+
|
|
34
|
+
// 初始化 AGENTS.md 模板
|
|
35
|
+
this.injectAgentsTemplate(project);
|
|
36
|
+
|
|
37
|
+
// 初始化各模块
|
|
38
|
+
for (const module of modules) {
|
|
39
|
+
switch (module.trim()) {
|
|
40
|
+
case "context":
|
|
41
|
+
this.initContext(project);
|
|
42
|
+
break;
|
|
43
|
+
case "bus":
|
|
44
|
+
await this.initBus(project);
|
|
45
|
+
break;
|
|
46
|
+
case "resources":
|
|
47
|
+
this.initResources(project);
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
console.error(`Unknown module: ${module}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
console.log("✓ Initialization complete");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 确保 AGENTS.md 和 CLAUDE.md 存在
|
|
60
|
+
*/
|
|
61
|
+
ensureAgentsFiles(project) {
|
|
62
|
+
const agentsFile = path.join(project, "AGENTS.md");
|
|
63
|
+
const claudeFile = path.join(project, "CLAUDE.md");
|
|
64
|
+
|
|
65
|
+
// 创建 AGENTS.md(如果不存在)
|
|
66
|
+
if (!fs.existsSync(agentsFile)) {
|
|
67
|
+
const content = `# Project Instructions
|
|
68
|
+
|
|
69
|
+
\`CLAUDE.md\` points to this file. Please keep project instructions here (prefer edits in \`AGENTS.md\`).
|
|
70
|
+
|
|
71
|
+
`;
|
|
72
|
+
fs.writeFileSync(agentsFile, content, "utf8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// CLAUDE.md 指向 AGENTS.md
|
|
76
|
+
fs.writeFileSync(claudeFile, "AGENTS.md\n", "utf8");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 初始化核心 .ufoo 目录
|
|
81
|
+
*/
|
|
82
|
+
initCore(project) {
|
|
83
|
+
console.log("[core] Initializing .ufoo core...");
|
|
84
|
+
|
|
85
|
+
const ufooDir = path.join(project, ".ufoo");
|
|
86
|
+
if (!fs.existsSync(ufooDir)) {
|
|
87
|
+
fs.mkdirSync(ufooDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 创建 docs 符号链接
|
|
91
|
+
const docsLink = path.join(ufooDir, "docs");
|
|
92
|
+
const docsTarget = path.join(this.repoRoot, "docs");
|
|
93
|
+
|
|
94
|
+
if (fs.existsSync(docsTarget)) {
|
|
95
|
+
if (fs.existsSync(docsLink)) {
|
|
96
|
+
fs.unlinkSync(docsLink);
|
|
97
|
+
}
|
|
98
|
+
fs.symlinkSync(docsTarget, docsLink);
|
|
99
|
+
console.log(`[core] Created docs symlink: .ufoo/docs -> ${docsTarget}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("[core] Done");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 注入 ufoo 模板到 AGENTS.md
|
|
107
|
+
*/
|
|
108
|
+
injectAgentsTemplate(project) {
|
|
109
|
+
if (!fs.existsSync(this.agentsTemplate)) {
|
|
110
|
+
console.log("[template] AGENTS.template.md not found, skipping");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log("[template] Injecting ufoo template into AGENTS.md...");
|
|
115
|
+
|
|
116
|
+
const agentsFile = path.join(project, "AGENTS.md");
|
|
117
|
+
let content = fs.readFileSync(agentsFile, "utf8");
|
|
118
|
+
|
|
119
|
+
const template = fs.readFileSync(this.agentsTemplate, "utf8");
|
|
120
|
+
|
|
121
|
+
// 查找或添加 ufoo 标记块
|
|
122
|
+
const marker = "<!-- ufoo-template -->";
|
|
123
|
+
if (content.includes(marker)) {
|
|
124
|
+
// 替换现有块
|
|
125
|
+
const startIdx = content.indexOf(marker);
|
|
126
|
+
const endIdx = content.indexOf(marker, startIdx + marker.length);
|
|
127
|
+
|
|
128
|
+
if (endIdx !== -1) {
|
|
129
|
+
content =
|
|
130
|
+
content.slice(0, startIdx) +
|
|
131
|
+
`${marker}\n${template}\n${marker}` +
|
|
132
|
+
content.slice(endIdx + marker.length);
|
|
133
|
+
} else {
|
|
134
|
+
// 只有开始标记,追加到末尾
|
|
135
|
+
content += `\n${marker}\n${template}\n${marker}\n`;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// 追加到末尾
|
|
139
|
+
content += `\n${marker}\n${template}\n${marker}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(agentsFile, content, "utf8");
|
|
143
|
+
console.log("[template] Done");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 初始化 context 模块
|
|
148
|
+
*/
|
|
149
|
+
initContext(project) {
|
|
150
|
+
console.log("[context] Initializing decision-only context...");
|
|
151
|
+
|
|
152
|
+
const targetDir = path.join(project, ".ufoo", "context");
|
|
153
|
+
const decisionsDir = path.join(targetDir, "decisions");
|
|
154
|
+
const legacyDir = path.join(targetDir, "DECISIONS");
|
|
155
|
+
const indexFile = path.join(targetDir, "decisions.jsonl");
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(targetDir)) {
|
|
158
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
if (!fs.existsSync(decisionsDir) && fs.existsSync(legacyDir)) {
|
|
161
|
+
fs.renameSync(legacyDir, decisionsDir);
|
|
162
|
+
}
|
|
163
|
+
if (!fs.existsSync(decisionsDir)) {
|
|
164
|
+
fs.mkdirSync(decisionsDir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
if (!fs.existsSync(indexFile)) {
|
|
167
|
+
fs.writeFileSync(indexFile, "", "utf8");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log("[context] Done");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 初始化 bus 模块
|
|
175
|
+
*/
|
|
176
|
+
async initBus(project) {
|
|
177
|
+
console.log("[bus] Initializing bus module...");
|
|
178
|
+
|
|
179
|
+
const EventBus = require("../bus");
|
|
180
|
+
const bus = new EventBus(project);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await bus.init();
|
|
184
|
+
console.log("[bus] Done");
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`[bus] Error: ${err.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 初始化 resources 模块
|
|
192
|
+
*/
|
|
193
|
+
initResources(project) {
|
|
194
|
+
if (!fs.existsSync(this.resourcesMod)) {
|
|
195
|
+
console.log("[resources] Module not found, skipping");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log("[resources] Initializing resources module...");
|
|
200
|
+
|
|
201
|
+
const targetDir = path.join(project, ".ufoo", "resources");
|
|
202
|
+
|
|
203
|
+
// 复制模块内容
|
|
204
|
+
this.copyModuleContent(this.resourcesMod, targetDir);
|
|
205
|
+
|
|
206
|
+
console.log("[resources] Done");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 复制模块内容
|
|
211
|
+
*/
|
|
212
|
+
copyModuleContent(src, dest) {
|
|
213
|
+
if (!fs.existsSync(dest)) {
|
|
214
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 复制所有文件和目录(排除 .git、node_modules 等)
|
|
218
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
219
|
+
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
// 跳过特殊目录
|
|
222
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const srcPath = path.join(src, entry.name);
|
|
227
|
+
const destPath = path.join(dest, entry.name);
|
|
228
|
+
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
this.copyRecursive(srcPath, destPath);
|
|
231
|
+
} else {
|
|
232
|
+
fs.copyFileSync(srcPath, destPath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 递归复制目录
|
|
239
|
+
*/
|
|
240
|
+
copyRecursive(src, dest) {
|
|
241
|
+
if (!fs.existsSync(dest)) {
|
|
242
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
246
|
+
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const srcPath = path.join(src, entry.name);
|
|
253
|
+
const destPath = path.join(dest, entry.name);
|
|
254
|
+
|
|
255
|
+
if (entry.isDirectory()) {
|
|
256
|
+
this.copyRecursive(srcPath, destPath);
|
|
257
|
+
} else {
|
|
258
|
+
fs.copyFileSync(srcPath, destPath);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = UfooInit;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 技能管理
|
|
6
|
+
*/
|
|
7
|
+
class SkillsManager {
|
|
8
|
+
constructor(repoRoot) {
|
|
9
|
+
this.repoRoot = repoRoot;
|
|
10
|
+
this.skillRoots = this.findSkillRoots();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 查找所有技能根目录
|
|
15
|
+
*/
|
|
16
|
+
findSkillRoots() {
|
|
17
|
+
const roots = [];
|
|
18
|
+
|
|
19
|
+
// 检查 SKILLS 目录
|
|
20
|
+
const mainSkills = path.join(this.repoRoot, "SKILLS");
|
|
21
|
+
if (fs.existsSync(mainSkills)) {
|
|
22
|
+
roots.push(mainSkills);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 检查 modules 中的 SKILLS
|
|
26
|
+
const modulesDir = path.join(this.repoRoot, "modules");
|
|
27
|
+
if (fs.existsSync(modulesDir)) {
|
|
28
|
+
const modules = fs.readdirSync(modulesDir);
|
|
29
|
+
for (const module of modules) {
|
|
30
|
+
const moduleSkills = path.join(modulesDir, module, "SKILLS");
|
|
31
|
+
if (fs.existsSync(moduleSkills)) {
|
|
32
|
+
roots.push(moduleSkills);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return roots;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 列出所有技能
|
|
42
|
+
*/
|
|
43
|
+
list() {
|
|
44
|
+
const skills = new Set();
|
|
45
|
+
|
|
46
|
+
for (const root of this.skillRoots) {
|
|
47
|
+
if (!fs.existsSync(root)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
skills.add(entry.name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Array.from(skills).sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 查找技能路径
|
|
64
|
+
*/
|
|
65
|
+
findSkill(name) {
|
|
66
|
+
for (const root of this.skillRoots) {
|
|
67
|
+
const skillPath = path.join(root, name);
|
|
68
|
+
if (fs.existsSync(skillPath)) {
|
|
69
|
+
return skillPath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 安装技能
|
|
77
|
+
*/
|
|
78
|
+
async install(name, options = {}) {
|
|
79
|
+
// 确定目标目录
|
|
80
|
+
let target = options.target;
|
|
81
|
+
|
|
82
|
+
if (!target) {
|
|
83
|
+
if (options.codex) {
|
|
84
|
+
const codexHome = process.env.CODEX_HOME || path.join(process.env.HOME, ".codex");
|
|
85
|
+
target = path.join(codexHome, "skills");
|
|
86
|
+
} else if (options.agents) {
|
|
87
|
+
target = path.join(process.env.HOME, ".agents", "skills");
|
|
88
|
+
} else {
|
|
89
|
+
target = path.join(process.env.HOME, ".claude", "skills");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`Installing to: ${target}`);
|
|
94
|
+
|
|
95
|
+
// 确保目标目录存在
|
|
96
|
+
if (!fs.existsSync(target)) {
|
|
97
|
+
fs.mkdirSync(target, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (name === "all") {
|
|
101
|
+
// 安装所有技能
|
|
102
|
+
const skills = this.list();
|
|
103
|
+
for (const skill of skills) {
|
|
104
|
+
await this.installOne(skill, target);
|
|
105
|
+
}
|
|
106
|
+
console.log(`\nInstalled ${skills.length} skills to ${target}`);
|
|
107
|
+
} else {
|
|
108
|
+
// 安装单个技能
|
|
109
|
+
await this.installOne(name, target);
|
|
110
|
+
console.log(`\nInstalled "${name}" to ${target}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 安装单个技能
|
|
116
|
+
*/
|
|
117
|
+
async installOne(name, target) {
|
|
118
|
+
const sourcePath = this.findSkill(name);
|
|
119
|
+
if (!sourcePath) {
|
|
120
|
+
throw new Error(`Skill not found: ${name}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const targetPath = path.join(target, name);
|
|
124
|
+
|
|
125
|
+
// 如果目标已存在,先删除
|
|
126
|
+
if (fs.existsSync(targetPath)) {
|
|
127
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 复制技能目录
|
|
131
|
+
console.log(` - ${name}`);
|
|
132
|
+
this.copyRecursive(sourcePath, targetPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 递归复制目录
|
|
137
|
+
*/
|
|
138
|
+
copyRecursive(src, dest) {
|
|
139
|
+
// 创建目标目录
|
|
140
|
+
if (!fs.existsSync(dest)) {
|
|
141
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
145
|
+
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const srcPath = path.join(src, entry.name);
|
|
148
|
+
const destPath = path.join(dest, entry.name);
|
|
149
|
+
|
|
150
|
+
if (entry.isDirectory()) {
|
|
151
|
+
this.copyRecursive(srcPath, destPath);
|
|
152
|
+
} else {
|
|
153
|
+
fs.copyFileSync(srcPath, destPath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = SkillsManager;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { readJSON } = require("../bus/utils");
|
|
4
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 显示项目状态
|
|
8
|
+
*/
|
|
9
|
+
class StatusDisplay {
|
|
10
|
+
constructor(projectRoot) {
|
|
11
|
+
this.projectRoot = projectRoot;
|
|
12
|
+
this.paths = getUfooPaths(projectRoot);
|
|
13
|
+
this.ufooDir = this.paths.ufooDir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 检查 .ufoo 目录是否存在
|
|
18
|
+
*/
|
|
19
|
+
checkUfooDir() {
|
|
20
|
+
if (!fs.existsSync(this.ufooDir)) {
|
|
21
|
+
console.error("FAIL: .ufoo not found. Run: ufoo init");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 获取当前订阅者信息
|
|
28
|
+
*/
|
|
29
|
+
getCurrentSubscriber() {
|
|
30
|
+
// 优先使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
|
|
31
|
+
if (process.env.UFOO_SUBSCRIBER_ID) {
|
|
32
|
+
return process.env.UFOO_SUBSCRIBER_ID;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const agentsFile = this.paths.agentsFile;
|
|
36
|
+
if (!fs.existsSync(agentsFile)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 尝试通过 tty 查找订阅者
|
|
41
|
+
let currentTty = null;
|
|
42
|
+
try {
|
|
43
|
+
currentTty = fs.readFileSync("/dev/tty", "utf8").trim();
|
|
44
|
+
} catch {
|
|
45
|
+
// tty 不可用
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentTty && currentTty.startsWith("/dev/")) {
|
|
49
|
+
const busData = readJSON(agentsFile);
|
|
50
|
+
if (busData && busData.agents) {
|
|
51
|
+
for (const [id, meta] of Object.entries(busData.agents)) {
|
|
52
|
+
if (meta.tty === currentTty) {
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 统计未读消息
|
|
64
|
+
*/
|
|
65
|
+
countUnreadMessages() {
|
|
66
|
+
const queuesDir = this.paths.busQueuesDir;
|
|
67
|
+
if (!fs.existsSync(queuesDir)) {
|
|
68
|
+
return { total: 0, details: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const agentsFile = this.paths.agentsFile;
|
|
72
|
+
const busData = readJSON(agentsFile, {});
|
|
73
|
+
|
|
74
|
+
let total = 0;
|
|
75
|
+
const details = [];
|
|
76
|
+
|
|
77
|
+
const subscribers = fs.readdirSync(queuesDir);
|
|
78
|
+
for (const safeName of subscribers) {
|
|
79
|
+
const pendingFile = path.join(queuesDir, safeName, "pending.jsonl");
|
|
80
|
+
if (!fs.existsSync(pendingFile)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const stat = fs.statSync(pendingFile);
|
|
85
|
+
if (stat.size === 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const content = fs.readFileSync(pendingFile, "utf8").trim();
|
|
90
|
+
const count = content ? content.split("\n").length : 0;
|
|
91
|
+
|
|
92
|
+
if (count > 0) {
|
|
93
|
+
total += count;
|
|
94
|
+
|
|
95
|
+
// 找到订阅者名称
|
|
96
|
+
let subscriberName = safeName.replace(/_/, ":");
|
|
97
|
+
if (busData.agents) {
|
|
98
|
+
for (const [id, meta] of Object.entries(busData.agents)) {
|
|
99
|
+
if (id.replace(/:/, "_") === safeName) {
|
|
100
|
+
subscriberName = id;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
details.push({ subscriber: subscriberName, count });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { total, details };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 统计开放的决策
|
|
115
|
+
*/
|
|
116
|
+
countOpenDecisions() {
|
|
117
|
+
const DecisionsManager = require("../context/decisions");
|
|
118
|
+
const manager = new DecisionsManager(this.projectRoot);
|
|
119
|
+
const decisionsDir = manager.decisionsDir;
|
|
120
|
+
if (!fs.existsSync(decisionsDir)) {
|
|
121
|
+
return { total: 0, details: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let total = 0;
|
|
125
|
+
const details = [];
|
|
126
|
+
|
|
127
|
+
const files = fs.readdirSync(decisionsDir)
|
|
128
|
+
.filter((f) => f.endsWith(".md"))
|
|
129
|
+
.sort();
|
|
130
|
+
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
const filePath = path.join(decisionsDir, file);
|
|
133
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
134
|
+
|
|
135
|
+
// 提取状态
|
|
136
|
+
const status = this.extractStatus(content);
|
|
137
|
+
if (status === "open") {
|
|
138
|
+
total++;
|
|
139
|
+
|
|
140
|
+
// 提取标题
|
|
141
|
+
const title = this.extractTitle(content);
|
|
142
|
+
details.push({ file, title: title || "(no title)" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { total, details };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 从决策文件提取状态
|
|
151
|
+
*/
|
|
152
|
+
extractStatus(content) {
|
|
153
|
+
const lines = content.split("\n");
|
|
154
|
+
let inFrontmatter = false;
|
|
155
|
+
let frontmatterCount = 0;
|
|
156
|
+
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
if (line.trim() === "---") {
|
|
159
|
+
frontmatterCount++;
|
|
160
|
+
if (frontmatterCount === 2) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
inFrontmatter = frontmatterCount === 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (inFrontmatter && line.startsWith("status:")) {
|
|
168
|
+
return line.split(":")[1].trim();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return "open";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 从决策文件提取标题
|
|
177
|
+
*/
|
|
178
|
+
extractTitle(content) {
|
|
179
|
+
const lines = content.split("\n");
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
if (line.startsWith("#")) {
|
|
182
|
+
return line.replace(/^#\s*/, "").trim();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 获取订阅者昵称(如果存在)
|
|
190
|
+
*/
|
|
191
|
+
getSubscriberNickname(subscriber) {
|
|
192
|
+
if (!subscriber) return null;
|
|
193
|
+
const agentsFile = this.paths.agentsFile;
|
|
194
|
+
const busData = readJSON(agentsFile);
|
|
195
|
+
if (!busData || !busData.agents) return null;
|
|
196
|
+
const meta = busData.agents[subscriber];
|
|
197
|
+
return meta && meta.nickname ? meta.nickname : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 显示横幅(如果存在)
|
|
202
|
+
*/
|
|
203
|
+
showBanner(subscriber) {
|
|
204
|
+
if (!subscriber) {
|
|
205
|
+
console.log("=== ufoo status ===");
|
|
206
|
+
console.log();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
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
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 显示完整状态
|
|
220
|
+
*/
|
|
221
|
+
async show() {
|
|
222
|
+
this.checkUfooDir();
|
|
223
|
+
|
|
224
|
+
const subscriber = this.getCurrentSubscriber();
|
|
225
|
+
|
|
226
|
+
// 显示横幅
|
|
227
|
+
this.showBanner(subscriber);
|
|
228
|
+
|
|
229
|
+
// 显示项目路径
|
|
230
|
+
console.log(`Project: ${this.projectRoot}`);
|
|
231
|
+
|
|
232
|
+
// 显示未读消息
|
|
233
|
+
const unread = this.countUnreadMessages();
|
|
234
|
+
console.log(`Unread messages: ${unread.total}`);
|
|
235
|
+
if (unread.details.length > 0) {
|
|
236
|
+
for (const { subscriber: sub, count } of unread.details) {
|
|
237
|
+
console.log(` - ${sub}: ${count}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 显示开放的决策
|
|
242
|
+
const decisions = this.countOpenDecisions();
|
|
243
|
+
console.log(`Open decisions: ${decisions.total}`);
|
|
244
|
+
if (decisions.details.length > 0) {
|
|
245
|
+
for (const { file, title } of decisions.details) {
|
|
246
|
+
console.log(` - ${file}: ${title}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = StatusDisplay;
|