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
package/src/agent/cliRunner.js
CHANGED
|
@@ -32,10 +32,18 @@ function runCommand(command, args, options = {}) {
|
|
|
32
32
|
let stdout = "";
|
|
33
33
|
let stderr = "";
|
|
34
34
|
child.stdout.on("data", (d) => {
|
|
35
|
-
|
|
35
|
+
const chunk = d.toString("utf8");
|
|
36
|
+
stdout += chunk;
|
|
37
|
+
if (options.onStdout) {
|
|
38
|
+
options.onStdout(chunk);
|
|
39
|
+
}
|
|
36
40
|
});
|
|
37
41
|
child.stderr.on("data", (d) => {
|
|
38
|
-
|
|
42
|
+
const chunk = d.toString("utf8");
|
|
43
|
+
stderr += chunk;
|
|
44
|
+
if (options.onStderr) {
|
|
45
|
+
options.onStderr(chunk);
|
|
46
|
+
}
|
|
39
47
|
});
|
|
40
48
|
let timeout = null;
|
|
41
49
|
if (options.timeoutMs) {
|
|
@@ -74,7 +82,7 @@ const DEFAULT_CLAUDE = {
|
|
|
74
82
|
"--dangerously-skip-permissions",
|
|
75
83
|
"--no-session-persistence",
|
|
76
84
|
"--json-schema",
|
|
77
|
-
'{"type":"object","properties":{"reply":{"type":"string"},"dispatch":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string"},"message":{"type":"string"}},"required":["target","message"]}},"ops":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"agent":{"type":"string"},"count":{"type":"integer"},"agent_id":{"type":"string"},"nickname":{"type":"string"}},"required":["action"]}},"disambiguate":{"type":"object","properties":{"prompt":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"agent_id":{"type":"string"},"reason":{"type":"string"}},"required":["agent_id"]}}}}},"required":["reply","dispatch","ops"]}',
|
|
85
|
+
'{"type":"object","properties":{"reply":{"type":"string"},"dispatch":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string"},"message":{"type":"string"}},"required":["target","message"]}},"ops":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["launch","close","rename"]},"agent":{"type":"string"},"count":{"type":"integer"},"agent_id":{"type":"string"},"nickname":{"type":"string"}},"required":["action"]}},"disambiguate":{"type":"object","properties":{"prompt":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"agent_id":{"type":"string"},"reason":{"type":"string"}},"required":["agent_id"]}}}}},"required":["reply","dispatch","ops"]}',
|
|
78
86
|
],
|
|
79
87
|
output: "json",
|
|
80
88
|
input: "arg",
|
|
@@ -111,6 +119,20 @@ function buildArgs(backend, prompt, opts) {
|
|
|
111
119
|
return { args, stdin: prompt };
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
function applySandboxOverride(args, sandbox) {
|
|
123
|
+
if (!sandbox) return;
|
|
124
|
+
const idx = args.indexOf("--sandbox");
|
|
125
|
+
if (idx >= 0) {
|
|
126
|
+
if (idx + 1 < args.length) {
|
|
127
|
+
args[idx + 1] = sandbox;
|
|
128
|
+
} else {
|
|
129
|
+
args.push(sandbox);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
args.push("--sandbox", sandbox);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
114
136
|
function isUnsupportedArgError(errText) {
|
|
115
137
|
const text = (errText || "").toLowerCase();
|
|
116
138
|
return text.includes("unknown option")
|
|
@@ -132,11 +154,14 @@ async function runCliAgent(params) {
|
|
|
132
154
|
systemPrompt: params.systemPrompt,
|
|
133
155
|
disableSession: params.disableSession,
|
|
134
156
|
});
|
|
157
|
+
if (backend === DEFAULT_CODEX && params.sandbox) {
|
|
158
|
+
applySandboxOverride(args, params.sandbox);
|
|
159
|
+
}
|
|
135
160
|
|
|
136
161
|
let res;
|
|
137
162
|
const env = { ...process.env, ...(params.env || {}) };
|
|
138
|
-
|
|
139
|
-
delete env.
|
|
163
|
+
// Clean up ufoo-specific env vars to avoid interference with CLI agents
|
|
164
|
+
delete env.UFOO_SUBSCRIBER_ID;
|
|
140
165
|
try {
|
|
141
166
|
res = await runCommand(backend.command, args, {
|
|
142
167
|
cwd: params.cwd,
|
|
@@ -161,6 +186,9 @@ async function runCliAgent(params) {
|
|
|
161
186
|
disableSession: params.disableSession,
|
|
162
187
|
},
|
|
163
188
|
);
|
|
189
|
+
if (params.sandbox) {
|
|
190
|
+
applySandboxOverride(retry.args, params.sandbox);
|
|
191
|
+
}
|
|
164
192
|
try {
|
|
165
193
|
res = await runCommand(backend.command, retry.args, {
|
|
166
194
|
cwd: params.cwd,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
3
4
|
const { spawnSync } = require("child_process");
|
|
4
|
-
const { randomBytes } = require("crypto");
|
|
5
5
|
const { runCliAgent } = require("./cliRunner");
|
|
6
6
|
const { normalizeCliOutput } = require("./normalizeOutput");
|
|
7
7
|
|
|
@@ -9,76 +9,79 @@ function sleep(ms) {
|
|
|
9
9
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function generateSessionId() {
|
|
13
|
-
return randomBytes(4).toString("hex");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
12
|
function buildEnv(agentType, sessionId, publisher, nickname) {
|
|
17
13
|
const env = { ...process.env };
|
|
18
|
-
if (agentType === "codex") {
|
|
19
|
-
env.CODEX_SESSION_ID = sessionId;
|
|
20
|
-
env.CLAUDE_SESSION_ID = "";
|
|
21
|
-
} else {
|
|
22
|
-
env.CLAUDE_SESSION_ID = sessionId;
|
|
23
|
-
env.CODEX_SESSION_ID = "";
|
|
24
|
-
}
|
|
25
14
|
env.AI_BUS_PUBLISHER = publisher || env.AI_BUS_PUBLISHER || "";
|
|
26
15
|
env.UFOO_NICKNAME = nickname || env.UFOO_NICKNAME || "";
|
|
27
16
|
env.UFOO_PARENT_PID = String(process.pid);
|
|
28
17
|
return env;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
throw new Error(err || "bus join failed");
|
|
20
|
+
function parseSubscriberId() {
|
|
21
|
+
// Daemon 已经注册,直接使用
|
|
22
|
+
if (process.env.UFOO_SUBSCRIBER_ID) {
|
|
23
|
+
const parts = process.env.UFOO_SUBSCRIBER_ID.split(":");
|
|
24
|
+
if (parts.length === 2) {
|
|
25
|
+
return {
|
|
26
|
+
subscriber: process.env.UFOO_SUBSCRIBER_ID,
|
|
27
|
+
agentType: parts[0],
|
|
28
|
+
sessionId: parts[1],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
43
31
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return { subscriber, env };
|
|
32
|
+
|
|
33
|
+
throw new Error("Internal runner requires UFOO_SUBSCRIBER_ID set by daemon");
|
|
47
34
|
}
|
|
48
35
|
|
|
49
36
|
function safeSubscriber(subscriber) {
|
|
50
37
|
return subscriber.replace(/:/g, "_");
|
|
51
38
|
}
|
|
52
39
|
|
|
53
|
-
function
|
|
40
|
+
function drainQueue(queueFile) {
|
|
54
41
|
if (!fs.existsSync(queueFile)) return [];
|
|
42
|
+
const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
43
|
+
let content = "";
|
|
44
|
+
let readOk = false;
|
|
55
45
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
fs.renameSync(queueFile, processingFile);
|
|
47
|
+
content = fs.readFileSync(processingFile, "utf8");
|
|
48
|
+
readOk = true;
|
|
59
49
|
} catch {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(processingFile)) {
|
|
52
|
+
fs.renameSync(processingFile, queueFile);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore rollback errors
|
|
56
|
+
}
|
|
60
57
|
return [];
|
|
58
|
+
} finally {
|
|
59
|
+
if (readOk) {
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(processingFile)) {
|
|
62
|
+
fs.rmSync(processingFile, { force: true });
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore cleanup errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
61
68
|
}
|
|
69
|
+
if (!content.trim()) return [];
|
|
70
|
+
return content.split(/\r?\n/).filter(Boolean);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
function
|
|
65
|
-
try {
|
|
66
|
-
fs.truncateSync(queueFile, 0);
|
|
67
|
-
} catch {
|
|
68
|
-
// ignore
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function handleEvent(projectRoot, agentType, provider, model, subscriber, sessionId, nickname, evt, cliSessionState) {
|
|
73
|
+
async function handleEvent(projectRoot, agentType, provider, model, subscriber, nickname, evt, cliSessionState) {
|
|
73
74
|
if (!evt || !evt.data || !evt.data.message) return;
|
|
74
75
|
const prompt = evt.data.message;
|
|
75
76
|
const publisher = evt.publisher || "unknown";
|
|
77
|
+
const sandbox = "workspace-write";
|
|
76
78
|
|
|
77
79
|
let res = await runCliAgent({
|
|
78
80
|
provider,
|
|
79
81
|
model,
|
|
80
82
|
prompt,
|
|
81
83
|
sessionId: cliSessionState.cliSessionId,
|
|
84
|
+
sandbox,
|
|
82
85
|
cwd: projectRoot,
|
|
83
86
|
});
|
|
84
87
|
|
|
@@ -95,6 +98,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
95
98
|
model,
|
|
96
99
|
prompt,
|
|
97
100
|
sessionId: null, // Let runCliAgent generate new session
|
|
101
|
+
sandbox,
|
|
98
102
|
cwd: projectRoot,
|
|
99
103
|
});
|
|
100
104
|
}
|
|
@@ -117,20 +121,17 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
117
121
|
|
|
118
122
|
spawnSync("ufoo", ["bus", "send", publisher, reply], {
|
|
119
123
|
cwd: projectRoot,
|
|
120
|
-
env:
|
|
124
|
+
env: { ...process.env, AI_BUS_PUBLISHER: subscriber },
|
|
121
125
|
stdio: "ignore",
|
|
122
126
|
});
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
126
|
-
|
|
130
|
+
// Internal runner 必须由 daemon 启动,UFOO_SUBSCRIBER_ID 应该已经设置
|
|
131
|
+
const { subscriber, agentType: parsedAgentType, sessionId } = parseSubscriberId();
|
|
127
132
|
const nickname = process.env.UFOO_NICKNAME || "";
|
|
128
|
-
const { subscriber } = joinBus(projectRoot, agentType, sessionId, nickname);
|
|
129
|
-
if (!subscriber) {
|
|
130
|
-
throw new Error("Failed to join bus for internal runner");
|
|
131
|
-
}
|
|
132
133
|
|
|
133
|
-
const queueDir = path.join(projectRoot
|
|
134
|
+
const queueDir = path.join(getUfooPaths(projectRoot).busQueuesDir, safeSubscriber(subscriber));
|
|
134
135
|
const queueFile = path.join(queueDir, "pending.jsonl");
|
|
135
136
|
const provider = agentType === "codex" ? "codex-cli" : "claude-cli";
|
|
136
137
|
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
@@ -138,7 +139,7 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
138
139
|
// Session state management for CLI continuity
|
|
139
140
|
// Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
|
|
140
141
|
const stableKey = nickname || `${agentType}-default`;
|
|
141
|
-
const sessionDir = path.join(projectRoot
|
|
142
|
+
const sessionDir = path.join(getUfooPaths(projectRoot).agentDir, "sessions");
|
|
142
143
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
143
144
|
const stateFile = path.join(sessionDir, `${stableKey}.json`);
|
|
144
145
|
|
|
@@ -155,6 +156,8 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
155
156
|
|
|
156
157
|
let running = true;
|
|
157
158
|
let processing = false;
|
|
159
|
+
let lastHeartbeat = 0;
|
|
160
|
+
const HEARTBEAT_INTERVAL = 30000; // 30秒心跳间隔
|
|
158
161
|
|
|
159
162
|
const stop = () => {
|
|
160
163
|
running = false;
|
|
@@ -165,11 +168,32 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
165
168
|
|
|
166
169
|
const cliSessionState = { cliSessionId, needsSave: false };
|
|
167
170
|
|
|
171
|
+
// 心跳更新函数
|
|
172
|
+
const updateHeartbeat = () => {
|
|
173
|
+
try {
|
|
174
|
+
spawnSync("ufoo", ["bus", "check", subscriber], {
|
|
175
|
+
cwd: projectRoot,
|
|
176
|
+
env: { ...process.env, UFOO_SUBSCRIBER_ID: subscriber },
|
|
177
|
+
stdio: "ignore",
|
|
178
|
+
timeout: 5000,
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
// ignore heartbeat errors
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
168
185
|
while (running) {
|
|
186
|
+
// 定期心跳更新
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
if (now - lastHeartbeat > HEARTBEAT_INTERVAL) {
|
|
189
|
+
updateHeartbeat();
|
|
190
|
+
lastHeartbeat = now;
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
if (!processing) {
|
|
170
194
|
processing = true;
|
|
171
195
|
try {
|
|
172
|
-
const lines =
|
|
196
|
+
const lines = drainQueue(queueFile);
|
|
173
197
|
if (lines.length > 0) {
|
|
174
198
|
const events = [];
|
|
175
199
|
for (const line of lines) {
|
|
@@ -179,11 +203,10 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
179
203
|
// ignore malformed line
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
|
-
truncateQueue(queueFile);
|
|
183
206
|
|
|
184
207
|
for (const evt of events) {
|
|
185
208
|
// eslint-disable-next-line no-await-in-loop
|
|
186
|
-
await handleEvent(projectRoot,
|
|
209
|
+
await handleEvent(projectRoot, parsedAgentType, provider, model, subscriber, nickname, evt, cliSessionState);
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
// Persist CLI session state after processing (only if changed and for claude)
|
|
@@ -199,6 +222,10 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
199
222
|
// ignore save errors
|
|
200
223
|
}
|
|
201
224
|
}
|
|
225
|
+
|
|
226
|
+
// 处理消息后更新心跳
|
|
227
|
+
updateHeartbeat();
|
|
228
|
+
lastHeartbeat = now;
|
|
202
229
|
}
|
|
203
230
|
} finally {
|
|
204
231
|
processing = false;
|