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.
- package/README.md +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +46 -42
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +1011 -1392
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +1162 -96
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -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 +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -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/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- 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: "
|
|
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
|
-
|
|
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 = {
|
|
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;
|