u-foo 1.0.0

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 (77) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +163 -0
  3. package/README.zh-CN.md +163 -0
  4. package/bin/uclaude +65 -0
  5. package/bin/ucodex +65 -0
  6. package/bin/ufoo +93 -0
  7. package/bin/ufoo.js +35 -0
  8. package/modules/AGENTS.template.md +87 -0
  9. package/modules/bus/README.md +132 -0
  10. package/modules/bus/SKILLS/ubus/SKILL.md +209 -0
  11. package/modules/bus/scripts/bus-alert.sh +185 -0
  12. package/modules/bus/scripts/bus-listen.sh +117 -0
  13. package/modules/context/ASSUMPTIONS.md +7 -0
  14. package/modules/context/CONSTRAINTS.md +7 -0
  15. package/modules/context/CONTEXT-STRUCTURE.md +49 -0
  16. package/modules/context/DECISION-PROTOCOL.md +62 -0
  17. package/modules/context/HANDOFF.md +33 -0
  18. package/modules/context/README.md +82 -0
  19. package/modules/context/RULES.md +15 -0
  20. package/modules/context/SKILLS/README.md +14 -0
  21. package/modules/context/SKILLS/uctx/SKILL.md +91 -0
  22. package/modules/context/SYSTEM.md +18 -0
  23. package/modules/context/TEMPLATES/assumptions.md +4 -0
  24. package/modules/context/TEMPLATES/constraints.md +4 -0
  25. package/modules/context/TEMPLATES/decision.md +16 -0
  26. package/modules/context/TEMPLATES/project-context-readme.md +6 -0
  27. package/modules/context/TEMPLATES/system.md +3 -0
  28. package/modules/context/TEMPLATES/terminology.md +4 -0
  29. package/modules/context/TERMINOLOGY.md +10 -0
  30. package/modules/resources/ICONS/README.md +12 -0
  31. package/modules/resources/ICONS/libraries/README.md +17 -0
  32. package/modules/resources/ICONS/libraries/heroicons/LICENSE +22 -0
  33. package/modules/resources/ICONS/libraries/heroicons/README.md +15 -0
  34. package/modules/resources/ICONS/libraries/heroicons/arrow-right.svg +4 -0
  35. package/modules/resources/ICONS/libraries/heroicons/check.svg +4 -0
  36. package/modules/resources/ICONS/libraries/heroicons/chevron-down.svg +4 -0
  37. package/modules/resources/ICONS/libraries/heroicons/cog-6-tooth.svg +5 -0
  38. package/modules/resources/ICONS/libraries/heroicons/magnifying-glass.svg +4 -0
  39. package/modules/resources/ICONS/libraries/heroicons/x-mark.svg +4 -0
  40. package/modules/resources/ICONS/libraries/lucide/LICENSE +40 -0
  41. package/modules/resources/ICONS/libraries/lucide/README.md +15 -0
  42. package/modules/resources/ICONS/libraries/lucide/arrow-right.svg +15 -0
  43. package/modules/resources/ICONS/libraries/lucide/check.svg +14 -0
  44. package/modules/resources/ICONS/libraries/lucide/chevron-down.svg +14 -0
  45. package/modules/resources/ICONS/libraries/lucide/search.svg +15 -0
  46. package/modules/resources/ICONS/libraries/lucide/settings.svg +15 -0
  47. package/modules/resources/ICONS/libraries/lucide/x.svg +15 -0
  48. package/modules/resources/ICONS/rules.md +7 -0
  49. package/modules/resources/README.md +9 -0
  50. package/modules/resources/UI/ANTI-PATTERNS.md +6 -0
  51. package/modules/resources/UI/TONE.md +6 -0
  52. package/package.json +40 -0
  53. package/scripts/banner.sh +89 -0
  54. package/scripts/bus-alert.sh +6 -0
  55. package/scripts/bus-autotrigger.sh +6 -0
  56. package/scripts/bus-daemon.sh +231 -0
  57. package/scripts/bus-inject.sh +144 -0
  58. package/scripts/bus-listen.sh +6 -0
  59. package/scripts/bus.sh +984 -0
  60. package/scripts/context-decisions.sh +167 -0
  61. package/scripts/context-doctor.sh +72 -0
  62. package/scripts/context-lint.sh +110 -0
  63. package/scripts/doctor.sh +22 -0
  64. package/scripts/init.sh +247 -0
  65. package/scripts/skills.sh +113 -0
  66. package/scripts/status.sh +125 -0
  67. package/src/agent/cliRunner.js +190 -0
  68. package/src/agent/internalRunner.js +212 -0
  69. package/src/agent/normalizeOutput.js +41 -0
  70. package/src/agent/ufooAgent.js +222 -0
  71. package/src/chat/index.js +1603 -0
  72. package/src/cli.js +349 -0
  73. package/src/config.js +37 -0
  74. package/src/daemon/index.js +501 -0
  75. package/src/daemon/ops.js +120 -0
  76. package/src/daemon/run.js +41 -0
  77. package/src/daemon/status.js +78 -0
@@ -0,0 +1,222 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { runCliAgent } = require("./cliRunner");
4
+ const { normalizeCliOutput } = require("./normalizeOutput");
5
+
6
+ function loadSessionState(projectRoot) {
7
+ const dir = path.join(projectRoot, ".ufoo", "agent");
8
+ const file = path.join(dir, "ufoo-agent.json");
9
+ try {
10
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
11
+ return { file, dir, data };
12
+ } catch {
13
+ return { file, dir, data: null };
14
+ }
15
+ }
16
+
17
+ function saveSessionState(projectRoot, state) {
18
+ const dir = path.join(projectRoot, ".ufoo", "agent");
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ fs.writeFileSync(path.join(dir, "ufoo-agent.json"), JSON.stringify(state, null, 2));
21
+ }
22
+
23
+ function isPidAlive(pid) {
24
+ if (!pid || typeof pid !== "number") return false;
25
+ try {
26
+ process.kill(pid, 0);
27
+ return true;
28
+ } catch (err) {
29
+ return Boolean(err && err.code === "EPERM");
30
+ }
31
+ }
32
+
33
+ function loadBusSummary(projectRoot, maxLines = 20) {
34
+ const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
35
+ let subscribers = [];
36
+ let nicknames = {};
37
+ try {
38
+ const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
39
+ subscribers = Object.entries(bus.subscribers || {})
40
+ .map(([id, meta]) => {
41
+ const pid = typeof meta.pid === "number" ? meta.pid : Number(meta.pid || 0);
42
+ const status = meta.status || "unknown";
43
+ const online = status === "active" && isPidAlive(pid);
44
+ const nickname = meta.nickname || "";
45
+ if (nickname) {
46
+ nicknames[nickname] = id;
47
+ }
48
+ return {
49
+ id,
50
+ status,
51
+ online,
52
+ agent_type: meta.agent_type || "",
53
+ nickname,
54
+ last_heartbeat: meta.last_heartbeat || "",
55
+ };
56
+ })
57
+ .filter((item) => item.online);
58
+ } catch {
59
+ subscribers = [];
60
+ nicknames = {};
61
+ }
62
+
63
+ const eventsDir = path.join(projectRoot, ".ufoo", "bus", "events");
64
+ let recent = [];
65
+ try {
66
+ const files = fs
67
+ .readdirSync(eventsDir)
68
+ .filter((f) => f.endsWith(".jsonl"))
69
+ .sort();
70
+ const lastFile = files[files.length - 1];
71
+ if (lastFile) {
72
+ const lines = fs
73
+ .readFileSync(path.join(eventsDir, lastFile), "utf8")
74
+ .trim()
75
+ .split(/\r?\n/)
76
+ .filter(Boolean);
77
+ recent = lines.slice(-maxLines);
78
+ }
79
+ } catch {
80
+ recent = [];
81
+ }
82
+
83
+ return { subscribers, nicknames, recent };
84
+ }
85
+
86
+ function buildSystemPrompt(context) {
87
+ return [
88
+ "You are ufoo-agent, a headless routing controller.",
89
+ "Return ONLY valid JSON. No extra text.",
90
+ "Schema:",
91
+ "{",
92
+ ' "reply": "string",',
93
+ ' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string"}],',
94
+ ' "ops": [{"action":"spawn|close","agent":"codex|claude","count":1,"agent_id":"id","nickname":"optional"}],',
95
+ ' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
96
+ "}",
97
+ "Rules:",
98
+ "- target must be 'broadcast', concrete agent-id, or a known nickname",
99
+ "- If multiple possible agents, use disambiguate with candidates and no dispatch.",
100
+ "- If user specifies a nickname for a new agent, include ops.spawn with nickname so daemon can rename.",
101
+ "- If no action needed, return reply with empty dispatch/ops.",
102
+ "",
103
+ "Context: online agents and recent bus events:",
104
+ JSON.stringify(context),
105
+ ].join("\n");
106
+ }
107
+
108
+ function loadHistory(projectRoot, maxTurns = 6) {
109
+ const file = path.join(projectRoot, ".ufoo", "agent", "ufoo-agent.history.jsonl");
110
+ try {
111
+ const lines = fs.readFileSync(file, "utf8").trim().split(/\r?\n/).filter(Boolean);
112
+ const items = lines.map((l) => JSON.parse(l));
113
+ return items.slice(-maxTurns);
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+
119
+ function appendHistory(projectRoot, item) {
120
+ const dir = path.join(projectRoot, ".ufoo", "agent");
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ const file = path.join(dir, "ufoo-agent.history.jsonl");
123
+ fs.appendFileSync(file, `${JSON.stringify(item)}\n`);
124
+ }
125
+
126
+ function buildHistoryPrompt(history) {
127
+ if (!history.length) return "";
128
+ const lines = ["Recent conversation:"];
129
+ for (const h of history) {
130
+ lines.push(`User: ${h.prompt}`);
131
+ if (h.reply) lines.push(`Agent: ${h.reply}`);
132
+ }
133
+ lines.push("");
134
+ return lines.join("\n");
135
+ }
136
+
137
+ function extractNickname(prompt) {
138
+ if (!prompt) return "";
139
+ const patterns = [
140
+ /(?:叫|名为|叫做|取名|昵称)\s*([A-Za-z0-9_-]{1,32})/i,
141
+ /(?:named|name)\s+([A-Za-z0-9_-]{1,32})/i,
142
+ ];
143
+ for (const re of patterns) {
144
+ const match = prompt.match(re);
145
+ if (match && match[1]) return match[1];
146
+ }
147
+ const quoted = prompt.match(/[“"']([^“"'\\]{1,32})[”"']/);
148
+ if (quoted && quoted[1]) return quoted[1];
149
+ return "";
150
+ }
151
+
152
+ async function runUfooAgent({ projectRoot, prompt, provider, model }) {
153
+ const state = loadSessionState(projectRoot);
154
+ const bus = loadBusSummary(projectRoot);
155
+ const systemPrompt = buildSystemPrompt(bus);
156
+ const history = loadHistory(projectRoot);
157
+ const historyPrompt = buildHistoryPrompt(history);
158
+ const fullPrompt = historyPrompt ? `${historyPrompt}User: ${prompt}` : prompt;
159
+
160
+ let res = await runCliAgent({
161
+ provider,
162
+ model,
163
+ prompt: fullPrompt,
164
+ systemPrompt,
165
+ sessionId: state.data?.sessionId,
166
+ disableSession: provider === "claude-cli",
167
+ cwd: projectRoot,
168
+ });
169
+
170
+ if (!res.ok) {
171
+ const msg = (res.error || "").toLowerCase();
172
+ if (msg.includes("session id") || msg.includes("session-id") || msg.includes("already in use")) {
173
+ res = await runCliAgent({
174
+ provider,
175
+ model,
176
+ prompt: fullPrompt,
177
+ systemPrompt,
178
+ sessionId: undefined,
179
+ disableSession: provider === "claude-cli",
180
+ cwd: projectRoot,
181
+ });
182
+ }
183
+ }
184
+
185
+ if (!res.ok) {
186
+ return { ok: false, error: res.error };
187
+ }
188
+
189
+ const text = normalizeCliOutput(res.output);
190
+ let payload = null;
191
+ try {
192
+ payload = JSON.parse(text);
193
+ } catch {
194
+ // Best-effort fallback to plain reply if model didn't return JSON.
195
+ // eslint-disable-next-line no-console
196
+ console.warn("[ufoo-agent] Non-JSON output received; using raw text reply.");
197
+ payload = { reply: text, dispatch: [], ops: [] };
198
+ }
199
+
200
+ const fallbackNickname = extractNickname(prompt);
201
+ if (fallbackNickname && payload && Array.isArray(payload.ops)) {
202
+ for (const op of payload.ops) {
203
+ if (op && op.action === "spawn" && !op.nickname) {
204
+ op.nickname = fallbackNickname;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ saveSessionState(projectRoot, {
211
+ provider,
212
+ model,
213
+ sessionId: res.sessionId,
214
+ updated_at: new Date().toISOString(),
215
+ });
216
+
217
+ appendHistory(projectRoot, { prompt, reply: payload.reply || "" });
218
+
219
+ return { ok: true, payload };
220
+ }
221
+
222
+ module.exports = { runUfooAgent };