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.
Files changed (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
@@ -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
- stdout += d.toString("utf8");
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
- stderr += d.toString("utf8");
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
- delete env.CLAUDE_SESSION_ID;
139
- delete env.CODEX_SESSION_ID;
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 joinBus(projectRoot, agentType, sessionId, nickname) {
32
- const env = buildEnv(agentType, sessionId, "", nickname);
33
- const args = ["bus", "join", sessionId, agentType === "codex" ? "codex" : "claude-code"];
34
- if (nickname) args.push(nickname);
35
- const res = spawnSync("ufoo", args, {
36
- cwd: projectRoot,
37
- env,
38
- stdio: ["ignore", "pipe", "pipe"],
39
- });
40
- if (res.status !== 0) {
41
- const err = (res.stderr || res.stdout || "").toString("utf8").trim();
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
- const out = (res.stdout || "").toString("utf8").trim().split(/\r?\n/);
45
- const subscriber = out[out.length - 1];
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 readQueue(queueFile) {
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
- const content = fs.readFileSync(queueFile, "utf8");
57
- if (!content.trim()) return [];
58
- return content.split(/\r?\n/).filter(Boolean);
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 truncateQueue(queueFile) {
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: buildEnv(agentType, sessionId, subscriber, nickname),
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
- const sessionId = generateSessionId();
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, ".ufoo", "bus", "queues", safeSubscriber(subscriber));
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, ".ufoo", "agent", "sessions");
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 = readQueue(queueFile);
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, agentType, provider, model, subscriber, sessionId, nickname, evt, cliSessionState);
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;