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
package/src/config.js CHANGED
@@ -2,19 +2,41 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
 
4
4
  const DEFAULT_CONFIG = {
5
- launchMode: "terminal",
5
+ launchMode: "auto",
6
6
  agentProvider: "codex-cli",
7
7
  agentModel: "",
8
+ assistantEngine: "auto",
9
+ assistantModel: "",
10
+ assistantUfooCmd: "",
11
+ ucodeProvider: "",
12
+ ucodeModel: "",
13
+ ucodeBaseUrl: "",
14
+ ucodeApiKey: "",
15
+ ucodeAgentDir: "",
16
+ autoResume: false,
8
17
  };
9
18
 
10
19
  function normalizeLaunchMode(value) {
11
- return value === "internal" ? "internal" : "terminal";
20
+ if (value === "auto") return "auto";
21
+ if (value === "internal") return "internal";
22
+ if (value === "tmux") return "tmux";
23
+ if (value === "terminal") return "terminal";
24
+ return "auto";
12
25
  }
13
26
 
14
27
  function normalizeAgentProvider(value) {
15
28
  return value === "claude-cli" ? "claude-cli" : "codex-cli";
16
29
  }
17
30
 
31
+ function normalizeAssistantEngine(value) {
32
+ const raw = String(value || "").trim().toLowerCase();
33
+ if (!raw || raw === "auto") return "auto";
34
+ if (raw === "codex" || raw === "codex-cli" || raw === "codex-code") return "codex";
35
+ if (raw === "claude" || raw === "claude-cli" || raw === "claude-code") return "claude";
36
+ if (raw === "ufoo") return "ufoo";
37
+ return "auto";
38
+ }
39
+
18
40
  function configPath(projectRoot) {
19
41
  return path.join(projectRoot, ".ufoo", "config.json");
20
42
  }
@@ -27,6 +49,15 @@ function loadConfig(projectRoot) {
27
49
  ...raw,
28
50
  launchMode: normalizeLaunchMode(raw.launchMode),
29
51
  agentProvider: normalizeAgentProvider(raw.agentProvider),
52
+ assistantEngine: normalizeAssistantEngine(raw.assistantEngine),
53
+ assistantModel: typeof raw.assistantModel === "string" ? raw.assistantModel : "",
54
+ assistantUfooCmd: typeof raw.assistantUfooCmd === "string" ? raw.assistantUfooCmd : "",
55
+ ucodeProvider: typeof raw.ucodeProvider === "string" ? raw.ucodeProvider : "",
56
+ ucodeModel: typeof raw.ucodeModel === "string" ? raw.ucodeModel : "",
57
+ ucodeBaseUrl: typeof raw.ucodeBaseUrl === "string" ? raw.ucodeBaseUrl : "",
58
+ ucodeApiKey: typeof raw.ucodeApiKey === "string" ? raw.ucodeApiKey : "",
59
+ ucodeAgentDir: typeof raw.ucodeAgentDir === "string" ? raw.ucodeAgentDir : "",
60
+ autoResume: raw.autoResume !== false,
30
61
  };
31
62
  } catch {
32
63
  return { ...DEFAULT_CONFIG };
@@ -36,14 +67,36 @@ function loadConfig(projectRoot) {
36
67
  function saveConfig(projectRoot, config) {
37
68
  const target = configPath(projectRoot);
38
69
  fs.mkdirSync(path.dirname(target), { recursive: true });
70
+ let existing = {};
71
+ try {
72
+ existing = JSON.parse(fs.readFileSync(target, "utf8"));
73
+ } catch {
74
+ existing = {};
75
+ }
39
76
  const merged = {
40
77
  ...DEFAULT_CONFIG,
78
+ ...existing,
41
79
  ...config,
42
80
  };
43
81
  merged.launchMode = normalizeLaunchMode(merged.launchMode);
44
82
  merged.agentProvider = normalizeAgentProvider(merged.agentProvider);
83
+ merged.assistantEngine = normalizeAssistantEngine(merged.assistantEngine);
84
+ merged.assistantModel = typeof merged.assistantModel === "string" ? merged.assistantModel : "";
85
+ merged.assistantUfooCmd = typeof merged.assistantUfooCmd === "string" ? merged.assistantUfooCmd : "";
86
+ merged.ucodeProvider = typeof merged.ucodeProvider === "string" ? merged.ucodeProvider : "";
87
+ merged.ucodeModel = typeof merged.ucodeModel === "string" ? merged.ucodeModel : "";
88
+ merged.ucodeBaseUrl = typeof merged.ucodeBaseUrl === "string" ? merged.ucodeBaseUrl : "";
89
+ merged.ucodeApiKey = typeof merged.ucodeApiKey === "string" ? merged.ucodeApiKey : "";
90
+ merged.ucodeAgentDir = typeof merged.ucodeAgentDir === "string" ? merged.ucodeAgentDir : "";
91
+ merged.autoResume = merged.autoResume !== false;
45
92
  fs.writeFileSync(target, JSON.stringify(merged, null, 2));
46
93
  return merged;
47
94
  }
48
95
 
49
- module.exports = { loadConfig, saveConfig, normalizeLaunchMode, normalizeAgentProvider };
96
+ module.exports = {
97
+ loadConfig,
98
+ saveConfig,
99
+ normalizeLaunchMode,
100
+ normalizeAgentProvider,
101
+ normalizeAssistantEngine,
102
+ };
@@ -0,0 +1,324 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const matter = require("gray-matter");
4
+
5
+ /**
6
+ * 决策管理器
7
+ * 处理项目决策日志的读取、过滤和显示
8
+ */
9
+ class DecisionsManager {
10
+ constructor(projectRoot) {
11
+ this.projectRoot = projectRoot;
12
+ this.contextDir = path.join(projectRoot, ".ufoo", "context");
13
+ this.decisionsDir = DecisionsManager.resolveDecisionsDir(
14
+ projectRoot,
15
+ this.contextDir
16
+ );
17
+ this.indexFile = path.join(this.contextDir, "decisions.jsonl");
18
+ }
19
+
20
+ /**
21
+ * 解析决策目录(优先小写 decisions,兼容旧 DECISIONS)
22
+ */
23
+ static resolveDecisionsDir(projectRoot, contextDir = null) {
24
+ if (process.env.AI_CONTEXT_DECISIONS_DIR) {
25
+ return process.env.AI_CONTEXT_DECISIONS_DIR;
26
+ }
27
+ const ctx = contextDir || path.join(projectRoot, ".ufoo", "context");
28
+ const lower = path.join(ctx, "decisions");
29
+ const upper = path.join(ctx, "DECISIONS");
30
+ if (fs.existsSync(lower)) return lower;
31
+ if (fs.existsSync(upper)) return upper;
32
+ return lower;
33
+ }
34
+
35
+ /**
36
+ * 读取所有决策文件
37
+ */
38
+ readDecisions() {
39
+ if (!fs.existsSync(this.decisionsDir)) {
40
+ return [];
41
+ }
42
+
43
+ const files = fs
44
+ .readdirSync(this.decisionsDir)
45
+ .filter((f) => f.endsWith(".md"))
46
+ .sort()
47
+ .reverse(); // Newest first
48
+
49
+ return files.map((file) => {
50
+ const filePath = path.join(this.decisionsDir, file);
51
+ const content = fs.readFileSync(filePath, "utf8");
52
+
53
+ let data = {};
54
+ let body = content;
55
+ let title = "";
56
+
57
+ try {
58
+ const parsed = matter(content);
59
+ data = parsed.data;
60
+ body = parsed.content;
61
+
62
+ // Extract title from first line of content
63
+ const firstLine = body.trim().split("\n")[0];
64
+ if (firstLine.startsWith("#")) {
65
+ title = firstLine.replace(/^#+\s*/, "").trim();
66
+ }
67
+ } catch {
68
+ // No frontmatter, extract title from first line
69
+ const firstLine = content.trim().split("\n")[0];
70
+ if (firstLine.startsWith("#")) {
71
+ title = firstLine.replace(/^#+\s*/, "").trim();
72
+ }
73
+ }
74
+
75
+ return {
76
+ file,
77
+ filePath,
78
+ status: data.status || "open",
79
+ title: title || "(no title)",
80
+ content,
81
+ data,
82
+ body,
83
+ };
84
+ });
85
+ }
86
+
87
+ /**
88
+ * 生成下一个 4 位编号
89
+ */
90
+ nextNumber() {
91
+ if (!fs.existsSync(this.decisionsDir)) {
92
+ return "0001";
93
+ }
94
+ const files = fs
95
+ .readdirSync(this.decisionsDir)
96
+ .filter((f) => f.endsWith(".md"))
97
+ .map((f) => {
98
+ const match = f.match(/^(\d{4})-/);
99
+ return match ? parseInt(match[1], 10) : 0;
100
+ });
101
+ const max = files.length ? Math.max(...files) : 0;
102
+ return String(max + 1).padStart(4, "0");
103
+ }
104
+
105
+ /**
106
+ * 简单 slugify
107
+ */
108
+ slugify(title) {
109
+ const cleaned = title
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9]+/g, "-")
112
+ .replace(/^-+|-+$/g, "")
113
+ .replace(/-+/g, "-");
114
+ return cleaned || "decision";
115
+ }
116
+
117
+ /**
118
+ * 创建新决策
119
+ */
120
+ createDecision(options = {}) {
121
+ const title = (options.title || "").trim();
122
+ if (!title) {
123
+ throw new Error("Missing title. Usage: ufoo ctx decisions new \"Title\"");
124
+ }
125
+
126
+ const author =
127
+ options.author ||
128
+ process.env.UFOO_NICKNAME ||
129
+ process.env.USER ||
130
+ process.env.USERNAME ||
131
+ "unknown";
132
+
133
+ const nicknameRaw =
134
+ options.nickname ||
135
+ process.env.UFOO_NICKNAME ||
136
+ process.env.USER ||
137
+ process.env.USERNAME ||
138
+ "unknown";
139
+
140
+ const status = options.status || "open";
141
+ const num = this.nextNumber();
142
+ const slug = this.slugify(title);
143
+ const nick = this.slugify(nicknameRaw);
144
+
145
+ fs.mkdirSync(this.contextDir, { recursive: true });
146
+ fs.mkdirSync(this.decisionsDir, { recursive: true });
147
+
148
+ const file = `${num}-${nick}-${slug}.md`;
149
+ const filePath = path.join(this.decisionsDir, file);
150
+ const date = new Date().toISOString().slice(0, 10);
151
+
152
+ const content =
153
+ `---\n` +
154
+ `status: ${status}\n` +
155
+ `nickname: ${nicknameRaw}\n` +
156
+ `---\n` +
157
+ `# DECISION ${num}: ${title}\n\n` +
158
+ `Date: ${date}\n` +
159
+ `Author: ${author}\n` +
160
+ `Nickname: ${nicknameRaw}\n\n` +
161
+ `Context:\nWhat led to this decision?\n\n` +
162
+ `Decision:\nWhat is now considered true?\n\n` +
163
+ `Implications:\nWhat must follow from this?\n`;
164
+
165
+ fs.writeFileSync(filePath, content, "utf8");
166
+ console.log(`Created ${filePath}`);
167
+
168
+ this.writeIndex();
169
+ return { file, filePath };
170
+ }
171
+
172
+ /**
173
+ * 从正文中提取字段(如 Date/Author)
174
+ */
175
+ extractField(body, fieldName) {
176
+ const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, "mi");
177
+ const match = body.match(regex);
178
+ return match ? match[1].trim() : "";
179
+ }
180
+
181
+ /**
182
+ * 规范化时间戳
183
+ */
184
+ normalizeTs(value, fallbackPath = null) {
185
+ if (value) {
186
+ const parsed = new Date(value);
187
+ if (!Number.isNaN(parsed.valueOf())) {
188
+ return parsed.toISOString();
189
+ }
190
+ return value;
191
+ }
192
+ if (fallbackPath && fs.existsSync(fallbackPath)) {
193
+ const stat = fs.statSync(fallbackPath);
194
+ return stat.mtime.toISOString();
195
+ }
196
+ return new Date().toISOString();
197
+ }
198
+
199
+ /**
200
+ * 构建决策索引(jsonl)
201
+ */
202
+ buildIndexEntries(decisions) {
203
+ const entries = [];
204
+
205
+ for (const d of decisions) {
206
+ const createdAt =
207
+ d.data.created_at ||
208
+ d.data.createdAt ||
209
+ this.extractField(d.body, "Date");
210
+ const author =
211
+ d.data.author ||
212
+ this.extractField(d.body, "Author") ||
213
+ d.data.resolved_by ||
214
+ d.data.resolvedBy ||
215
+ "";
216
+
217
+ entries.push({
218
+ ts: this.normalizeTs(createdAt, d.filePath),
219
+ type: "decision",
220
+ file: d.file,
221
+ author,
222
+ status: d.status,
223
+ title: d.title,
224
+ });
225
+
226
+ if (d.status && d.status !== "open") {
227
+ const resolvedAt = d.data.resolved_at || d.data.resolvedAt;
228
+ const resolvedBy = d.data.resolved_by || d.data.resolvedBy || author;
229
+ entries.push({
230
+ ts: this.normalizeTs(resolvedAt, d.filePath),
231
+ type: "decision_status",
232
+ file: d.file,
233
+ author: resolvedBy,
234
+ status: d.status,
235
+ title: d.title,
236
+ });
237
+ }
238
+ }
239
+
240
+ return entries;
241
+ }
242
+
243
+ /**
244
+ * 写入索引文件
245
+ */
246
+ writeIndex() {
247
+ const decisions = this.readDecisions();
248
+ const entries = this.buildIndexEntries(decisions);
249
+
250
+ fs.mkdirSync(this.contextDir, { recursive: true });
251
+
252
+ const lines = entries.map((e) => JSON.stringify(e));
253
+ const output = lines.length ? `${lines.join("\n")}\n` : "";
254
+ fs.writeFileSync(this.indexFile, output, "utf8");
255
+
256
+ console.log(`Wrote ${entries.length} entries to ${this.indexFile}`);
257
+ }
258
+
259
+ /**
260
+ * 过滤决策
261
+ */
262
+ filterDecisions(decisions, statusFilter = "open") {
263
+ if (statusFilter === "all") {
264
+ return decisions;
265
+ }
266
+
267
+ return decisions.filter((d) => d.status === statusFilter);
268
+ }
269
+
270
+ /**
271
+ * 列出决策(简要模式)
272
+ */
273
+ list(options = {}) {
274
+ const { status = "open" } = options;
275
+
276
+ const decisions = this.readDecisions();
277
+ const filtered = this.filterDecisions(decisions, status);
278
+
279
+ console.log(
280
+ `=== Decisions (${filtered.length} ${status}, ${decisions.length} total) ===`
281
+ );
282
+
283
+ for (const d of filtered) {
284
+ console.log(` [${d.status}] ${d.file}: ${d.title}`);
285
+ }
286
+
287
+ return filtered;
288
+ }
289
+
290
+ /**
291
+ * 显示决策(完整内容)
292
+ */
293
+ show(options = {}) {
294
+ const { status = "open", num = 1, all = false } = options;
295
+
296
+ const decisions = this.readDecisions();
297
+ const filtered = this.filterDecisions(decisions, status);
298
+
299
+ if (filtered.length === 0) {
300
+ if (decisions.length === 0) {
301
+ console.log("No decisions found.");
302
+ } else {
303
+ console.log(`No decisions with status '${status}' found.`);
304
+ }
305
+ return [];
306
+ }
307
+
308
+ console.log(`=== Latest Decision(s) [${status}] ===`);
309
+ console.log("");
310
+
311
+ const count = all ? filtered.length : Math.min(num, filtered.length);
312
+
313
+ for (let i = 0; i < count; i++) {
314
+ const d = filtered[i];
315
+ console.log(`--- ${d.file} [${d.status}] ---`);
316
+ console.log(d.content);
317
+ console.log("");
318
+ }
319
+
320
+ return filtered.slice(0, count);
321
+ }
322
+ }
323
+
324
+ module.exports = DecisionsManager;
@@ -0,0 +1,183 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const DecisionsManager = require("./decisions");
4
+
5
+ /**
6
+ * Context Doctor & Lint
7
+ * 诊断和验证 context 目录结构
8
+ */
9
+ class ContextDoctor {
10
+ constructor(projectRoot) {
11
+ this.projectRoot = projectRoot;
12
+ this.contextDir = path.join(projectRoot, ".ufoo", "context");
13
+ this.failed = false;
14
+ }
15
+
16
+ /**
17
+ * 失败检查
18
+ */
19
+ fail(message) {
20
+ console.error(`FAIL: ${message}`);
21
+ this.failed = true;
22
+ }
23
+
24
+ /**
25
+ * 检查文件存在
26
+ */
27
+ checkFile(filePath, name) {
28
+ if (!fs.existsSync(filePath)) {
29
+ this.fail(`Missing file: ${name || filePath}`);
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+
35
+ /**
36
+ * 检查目录存在
37
+ */
38
+ checkDir(dirPath, name) {
39
+ if (!fs.existsSync(dirPath)) {
40
+ this.fail(`Missing directory: ${name || dirPath}`);
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * 检查 glob 模式有匹配
48
+ */
49
+ checkAnyGlob(dir, pattern, name) {
50
+ try {
51
+ const files = fs.readdirSync(dir).filter((f) => f.match(pattern));
52
+ if (files.length === 0) {
53
+ this.fail(`Missing: ${name || pattern} in ${dir}`);
54
+ return false;
55
+ }
56
+ return true;
57
+ } catch {
58
+ this.fail(`Cannot read directory: ${dir}`);
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Lint 项目 context
65
+ */
66
+ lintProject(projectPath) {
67
+ const ctxPath = projectPath || this.contextDir;
68
+
69
+ console.log(`Linting project context: ${ctxPath}`);
70
+
71
+ // Check basic structure
72
+ this.checkDir(ctxPath, "context directory");
73
+ this.checkFile(path.join(ctxPath, "decisions.jsonl"), "decisions.jsonl");
74
+
75
+ // Check decisions directory
76
+ const decisionsDir = DecisionsManager.resolveDecisionsDir(
77
+ this.projectRoot,
78
+ ctxPath
79
+ );
80
+ this.checkDir(decisionsDir, "decisions directory");
81
+
82
+ return !this.failed;
83
+ }
84
+
85
+ /**
86
+ * Lint 协议 repo(modules/context)
87
+ */
88
+ lintProtocol() {
89
+ const moduleRoot = path.join(this.projectRoot, "modules", "context");
90
+
91
+ if (!fs.existsSync(moduleRoot)) {
92
+ console.log("No protocol module found (skipping protocol lint)");
93
+ return true;
94
+ }
95
+
96
+ console.log(`Linting protocol repo: ${moduleRoot}`);
97
+
98
+ // Check minimal module files
99
+ this.checkFile(path.join(moduleRoot, "README.md"), "README.md");
100
+ this.checkFile(
101
+ path.join(moduleRoot, "SKILLS", "uctx", "SKILL.md"),
102
+ "SKILLS/uctx/SKILL.md"
103
+ );
104
+
105
+ return !this.failed;
106
+ }
107
+
108
+ /**
109
+ * 运行完整诊断
110
+ */
111
+ async run(options = {}) {
112
+ const { mode = "protocol", projectPath = null } = options;
113
+
114
+ console.log("=== context doctor ===");
115
+ console.log(
116
+ "Reminder: If you provide evaluation/recommendation/plan, write a decision before replying."
117
+ );
118
+ console.log("");
119
+
120
+ this.failed = false;
121
+
122
+ if (mode === "project") {
123
+ if (!projectPath) {
124
+ this.fail("--project requires a path");
125
+ return false;
126
+ }
127
+
128
+ console.log("Mode: project");
129
+ console.log(`Project: ${projectPath}`);
130
+ this.lintProject(projectPath);
131
+
132
+ // Test decisions listing
133
+ try {
134
+ const decisionsManager = new DecisionsManager(this.projectRoot);
135
+ decisionsManager.decisionsDir = DecisionsManager.resolveDecisionsDir(
136
+ this.projectRoot,
137
+ projectPath
138
+ );
139
+ decisionsManager.show({ num: 1 });
140
+ } catch (err) {
141
+ this.fail(`Decisions check failed: ${err.message}`);
142
+ }
143
+ } else {
144
+ console.log("Mode: protocol");
145
+
146
+ // Check protocol module
147
+ this.lintProtocol();
148
+
149
+ // Test decisions listing (silent)
150
+ try {
151
+ const decisionsManager = new DecisionsManager(this.projectRoot);
152
+ decisionsManager.show({ num: 1 });
153
+ } catch {
154
+ // Silent
155
+ }
156
+ }
157
+
158
+ // Check global modules
159
+ const globalContext = path.join(
160
+ process.env.HOME,
161
+ ".ufoo",
162
+ "modules",
163
+ "context"
164
+ );
165
+ if (!fs.existsSync(globalContext)) {
166
+ console.log("");
167
+ console.log(
168
+ `WARN: ${globalContext} not found (install via ufoo for best UX)`
169
+ );
170
+ }
171
+
172
+ console.log("");
173
+ if (this.failed) {
174
+ console.log("Status: FAILED");
175
+ return false;
176
+ } else {
177
+ console.log("Status: OK");
178
+ return true;
179
+ }
180
+ }
181
+ }
182
+
183
+ module.exports = ContextDoctor;
@@ -0,0 +1,55 @@
1
+ const ContextDoctor = require("./doctor");
2
+ const DecisionsManager = require("./decisions");
3
+ const SyncManager = require("./sync");
4
+
5
+ /**
6
+ * Context management wrapper for chat commands
7
+ */
8
+ class UfooContext {
9
+ constructor(projectRoot) {
10
+ this.projectRoot = projectRoot;
11
+ this.doctorInstance = new ContextDoctor(projectRoot);
12
+ this.decisionsManager = new DecisionsManager(projectRoot);
13
+ this.syncManager = new SyncManager(projectRoot);
14
+ }
15
+
16
+ /**
17
+ * Run doctor check
18
+ */
19
+ async doctor() {
20
+ await this.doctorInstance.run({ mode: "project", projectPath: this.projectRoot });
21
+ }
22
+
23
+ /**
24
+ * List decisions
25
+ */
26
+ async listDecisions() {
27
+ this.decisionsManager.list({ status: "open" });
28
+ }
29
+
30
+ /**
31
+ * Get context status
32
+ */
33
+ async status() {
34
+ const decisions = this.decisionsManager.readDecisions();
35
+ const openDecisions = decisions.filter(d => d.status === "open");
36
+ const sync = this.syncManager.parseLines();
37
+ console.log(`Context: ${openDecisions.length} open decision(s), ${decisions.length} total, ${sync.length} sync note(s)`);
38
+ }
39
+
40
+ /**
41
+ * Append a sync note
42
+ */
43
+ async syncWrite(options = {}) {
44
+ return this.syncManager.write(options);
45
+ }
46
+
47
+ /**
48
+ * Show sync notes
49
+ */
50
+ async listSync(options = {}) {
51
+ return this.syncManager.list(options);
52
+ }
53
+ }
54
+
55
+ module.exports = UfooContext;