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
@@ -0,0 +1,291 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const EventBus = require("../bus");
5
+ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
6
+ const { getUfooPaths } = require("../ufoo/paths");
7
+
8
+ /**
9
+ * Build probe marker using nickname (e.g., "claude-47")
10
+ * Simpler than the old token format, easier to search
11
+ */
12
+ function buildProbeMarker(nickname) {
13
+ return nickname || "";
14
+ }
15
+
16
+ /**
17
+ * Build probe command: /ufoo <nickname> for claude-code, ufoo <nickname> for codex
18
+ */
19
+ function buildProbeCommand(agentType, nickname) {
20
+ const base = `ufoo ${nickname}`;
21
+ return agentType === "claude-code" ? `/${base}` : base;
22
+ }
23
+
24
+ function readLines(filePath) {
25
+ try {
26
+ const raw = fs.readFileSync(filePath, "utf8");
27
+ return raw.split(/\r?\n/).filter(Boolean);
28
+ } catch {
29
+ return [];
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Check if a history record contains our probe marker
35
+ * Searches for "/ufoo <marker>" or "ufoo <marker>" pattern
36
+ */
37
+ function recordContainsMarker(record, marker, rawLine) {
38
+ if (!marker) return false;
39
+
40
+ // Build both possible patterns
41
+ const patterns = [`/ufoo ${marker}`, `ufoo ${marker}`];
42
+
43
+ // Check raw line first (fastest)
44
+ if (rawLine) {
45
+ for (const pattern of patterns) {
46
+ if (rawLine.includes(pattern)) return true;
47
+ }
48
+ }
49
+
50
+ if (!record || typeof record !== "object") return false;
51
+
52
+ // Check common fields where user input might appear
53
+ const fields = [
54
+ record.display, // history.jsonl uses "display" for user input
55
+ record.text,
56
+ record.prompt,
57
+ record.input,
58
+ record.message,
59
+ record.query,
60
+ record.content,
61
+ ];
62
+
63
+ for (const field of fields) {
64
+ if (typeof field === "string") {
65
+ for (const pattern of patterns) {
66
+ if (field.includes(pattern)) return true;
67
+ }
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ function extractSessionId(record, rawLine) {
74
+ if (record && typeof record === "object") {
75
+ return record.session_id || record.sessionId || record.session || "";
76
+ }
77
+ if (typeof rawLine === "string") {
78
+ const match = rawLine.match(/"session(?:_id|Id)"\s*:\s*"([^"]+)"/);
79
+ if (match && match[1]) return match[1];
80
+ }
81
+ return "";
82
+ }
83
+
84
+ /**
85
+ * Find session ID in a history file by searching for the probe marker
86
+ */
87
+ function findSessionInFile(filePath, marker) {
88
+ if (!filePath || !fs.existsSync(filePath)) return null;
89
+ const lines = readLines(filePath);
90
+
91
+ // Search from end (most recent first)
92
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
93
+ const line = lines[i];
94
+
95
+ // Quick check: line must contain the marker string
96
+ if (!line.includes(marker)) continue;
97
+
98
+ let record = null;
99
+ try {
100
+ record = JSON.parse(line);
101
+ } catch {
102
+ record = null;
103
+ }
104
+
105
+ if (!recordContainsMarker(record, marker, line)) continue;
106
+
107
+ const sessionId = extractSessionId(record, line);
108
+ if (sessionId) {
109
+ return { sessionId, source: filePath };
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function getClaudeHistoryPath() {
116
+ return path.join(os.homedir(), ".claude", "history.jsonl");
117
+ }
118
+
119
+ function getCodexHistoryPath() {
120
+ return path.join(os.homedir(), ".codex", "history.jsonl");
121
+ }
122
+
123
+ /**
124
+ * Search provider history for the probe marker and return session ID
125
+ */
126
+ function resolveProviderSession(agentType, marker) {
127
+ if (agentType === "codex") {
128
+ return findSessionInFile(getCodexHistoryPath(), marker);
129
+ }
130
+ if (agentType === "claude-code") {
131
+ return findSessionInFile(getClaudeHistoryPath(), marker);
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Save probe marker to agents data (for debugging/tracking)
138
+ */
139
+ function persistProbeMarker(projectRoot, subscriberId, marker) {
140
+ const filePath = getUfooPaths(projectRoot).agentsFile;
141
+ const data = loadAgentsData(filePath);
142
+ const meta = data.agents[subscriberId] || {};
143
+ data.agents[subscriberId] = {
144
+ ...meta,
145
+ provider_session_probe: marker,
146
+ provider_session_updated_at: new Date().toISOString(),
147
+ };
148
+ saveAgentsData(filePath, data);
149
+ }
150
+
151
+ function persistProviderSession(projectRoot, subscriberId, payload) {
152
+ const filePath = getUfooPaths(projectRoot).agentsFile;
153
+ const data = loadAgentsData(filePath);
154
+ const meta = data.agents[subscriberId] || {};
155
+ data.agents[subscriberId] = {
156
+ ...meta,
157
+ provider_session_id: payload.sessionId || "",
158
+ provider_session_source: payload.source || "",
159
+ provider_session_updated_at: new Date().toISOString(),
160
+ };
161
+ saveAgentsData(filePath, data);
162
+ }
163
+
164
+ /**
165
+ * Retry searching for session ID with the given marker
166
+ */
167
+ async function resolveWithRetries(agentType, marker, attempts = 12, intervalMs = 2000) {
168
+ for (let i = 0; i < attempts; i += 1) {
169
+ const resolved = resolveProviderSession(agentType, marker);
170
+ if (resolved && resolved.sessionId) return resolved;
171
+ // eslint-disable-next-line no-await-in-loop
172
+ await new Promise((r) => setTimeout(r, intervalMs));
173
+ }
174
+ return null;
175
+ }
176
+
177
+
178
+ function loadProviderSessionCache(projectRoot) {
179
+ const filePath = getUfooPaths(projectRoot).agentsFile;
180
+ const data = loadAgentsData(filePath);
181
+ const cache = new Map();
182
+ for (const [id, meta] of Object.entries(data.agents || {})) {
183
+ if (meta && meta.provider_session_id) {
184
+ cache.set(id, {
185
+ sessionId: meta.provider_session_id,
186
+ source: meta.provider_session_source || "",
187
+ updated_at: meta.provider_session_updated_at || "",
188
+ });
189
+ }
190
+ }
191
+ return cache;
192
+ }
193
+
194
+ /**
195
+ * Execute probe: inject command and search for session ID
196
+ */
197
+ async function executeProbe({
198
+ projectRoot,
199
+ subscriberId,
200
+ agentType,
201
+ nickname,
202
+ attempts = 15,
203
+ intervalMs = 2000,
204
+ onResolved = null,
205
+ }) {
206
+ const marker = buildProbeMarker(nickname);
207
+
208
+ try {
209
+ const command = buildProbeCommand(agentType, nickname);
210
+ const bus = new EventBus(projectRoot);
211
+ bus.ensureBus();
212
+ await bus.inject(subscriberId, command);
213
+ } catch {
214
+ // ignore injection failures
215
+ }
216
+
217
+ const resolved = await resolveWithRetries(agentType, marker, attempts, intervalMs);
218
+ if (resolved && resolved.sessionId) {
219
+ persistProviderSession(projectRoot, subscriberId, resolved);
220
+ if (typeof onResolved === "function") {
221
+ onResolved(subscriberId, resolved);
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Schedule a provider session probe
228
+ *
229
+ * @param {Object} options
230
+ * @param {string} options.projectRoot - Project root directory
231
+ * @param {string} options.subscriberId - Subscriber ID (e.g., "claude-code:abc123")
232
+ * @param {string} options.agentType - Agent type ("claude-code" or "codex")
233
+ * @param {string} options.nickname - Agent nickname (e.g., "claude-47")
234
+ * @param {number} options.delayMs - Delay before injection
235
+ * @param {number} options.attempts - Number of search attempts
236
+ * @param {number} options.intervalMs - Interval between attempts
237
+ * @param {Function} options.onResolved - Callback when session ID is found
238
+ */
239
+ function scheduleProviderSessionProbe({
240
+ projectRoot,
241
+ subscriberId,
242
+ agentType,
243
+ nickname,
244
+ delayMs = 8000,
245
+ attempts = 15,
246
+ intervalMs = 2000,
247
+ onResolved = null,
248
+ }) {
249
+ if (!subscriberId || !agentType) return null;
250
+ if (agentType !== "codex" && agentType !== "claude-code") return null;
251
+ if (!nickname) return null;
252
+
253
+ const marker = buildProbeMarker(nickname);
254
+ persistProbeMarker(projectRoot, subscriberId, marker);
255
+
256
+ let executed = false;
257
+ let timer = null;
258
+
259
+ const execute = async () => {
260
+ if (executed) return;
261
+ executed = true;
262
+ if (timer) {
263
+ clearTimeout(timer);
264
+ timer = null;
265
+ }
266
+ await executeProbe({
267
+ projectRoot,
268
+ subscriberId,
269
+ agentType,
270
+ nickname,
271
+ attempts,
272
+ intervalMs,
273
+ onResolved,
274
+ });
275
+ };
276
+
277
+ // Schedule delayed execution (fallback)
278
+ timer = setTimeout(execute, delayMs);
279
+
280
+ // Return handle for early trigger
281
+ return {
282
+ subscriberId,
283
+ marker,
284
+ triggerNow: execute,
285
+ };
286
+ }
287
+
288
+ module.exports = {
289
+ scheduleProviderSessionProbe,
290
+ loadProviderSessionCache,
291
+ };
package/src/daemon/run.js CHANGED
@@ -9,6 +9,8 @@ function runDaemonCli(argv) {
9
9
  const provider = process.env.UFOO_AGENT_PROVIDER || config.agentProvider || "codex-cli";
10
10
  const model =
11
11
  process.env.UFOO_AGENT_MODEL || config.agentModel || (provider === "claude-cli" ? "opus" : "");
12
+ const resumeMode = process.env.UFOO_FORCE_RESUME === "1" ? "force" : "auto";
13
+ const launchMode = config.launchMode || "terminal";
12
14
 
13
15
  if (cmd === "start" || cmd === "--start") {
14
16
  if (isRunning(projectRoot)) return;
@@ -23,13 +25,44 @@ function runDaemonCli(argv) {
23
25
  child.unref();
24
26
  return;
25
27
  }
26
- startDaemon({ projectRoot, provider, model });
28
+ startDaemon({ projectRoot, provider, model, resumeMode });
27
29
  return;
28
30
  }
29
31
  if (cmd === "stop" || cmd === "--stop") {
30
32
  stopDaemon(projectRoot);
31
33
  return;
32
34
  }
35
+ if (cmd === "restart" || cmd === "--restart") {
36
+ // Stop if running
37
+ if (isRunning(projectRoot)) {
38
+ stopDaemon(projectRoot);
39
+ // Wait for clean shutdown
40
+ let attempts = 0;
41
+ while (isRunning(projectRoot) && attempts < 50) {
42
+ attempts++;
43
+ require("child_process").spawnSync("sleep", ["0.1"]);
44
+ }
45
+ }
46
+ // Start fresh daemon
47
+ if (!process.env.UFOO_DAEMON_CHILD) {
48
+ const { spawn } = require("child_process");
49
+ const forceResume = launchMode !== "terminal";
50
+ const childEnv = { ...process.env, UFOO_DAEMON_CHILD: "1" };
51
+ if (forceResume) childEnv.UFOO_FORCE_RESUME = "1";
52
+ const child = spawn(process.execPath, [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
53
+ detached: true,
54
+ stdio: "ignore",
55
+ env: childEnv,
56
+ cwd: projectRoot,
57
+ });
58
+ child.unref();
59
+ return;
60
+ }
61
+ // Skip auto-resume on restart in terminal mode to avoid reopening stale terminals.
62
+ const forceResume = launchMode !== "terminal";
63
+ startDaemon({ projectRoot, provider, model, resumeMode: forceResume ? "force" : "none" });
64
+ return;
65
+ }
33
66
  if (cmd === "status" || cmd === "--status") {
34
67
  const running = isRunning(projectRoot);
35
68
  // eslint-disable-next-line no-console
@@ -1,8 +1,10 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { getUfooPaths } = require("../ufoo/paths");
4
+ const { isMetaActive } = require("../bus/utils");
3
5
 
4
6
  function readBus(projectRoot) {
5
- const busPath = path.join(projectRoot, ".ufoo", "bus", "bus.json");
7
+ const busPath = getUfooPaths(projectRoot).agentsFile;
6
8
  try {
7
9
  return JSON.parse(fs.readFileSync(busPath, "utf8"));
8
10
  } catch {
@@ -11,7 +13,9 @@ function readBus(projectRoot) {
11
13
  }
12
14
 
13
15
  function readDecisions(projectRoot) {
14
- const dir = path.join(projectRoot, ".ufoo", "context", "DECISIONS");
16
+ const DecisionsManager = require("../context/decisions");
17
+ const manager = new DecisionsManager(projectRoot);
18
+ const dir = manager.decisionsDir;
15
19
  let open = 0;
16
20
  try {
17
21
  const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
@@ -28,7 +32,7 @@ function readDecisions(projectRoot) {
28
32
  }
29
33
 
30
34
  function readUnread(projectRoot) {
31
- const queuesDir = path.join(projectRoot, ".ufoo", "bus", "queues");
35
+ const queuesDir = getUfooPaths(projectRoot).busQueuesDir;
32
36
  let total = 0;
33
37
  const perSubscriber = {};
34
38
  try {
@@ -48,21 +52,34 @@ function readUnread(projectRoot) {
48
52
  return { total, perSubscriber };
49
53
  }
50
54
 
55
+ function isHiddenSubscriber(id, meta) {
56
+ if (!id) return false;
57
+ if (id === "ufoo-agent") return true;
58
+ if (meta && meta.nickname === "ufoo-agent") return true;
59
+ if (meta && meta.agent_type === "ufoo-agent") return true;
60
+ return false;
61
+ }
62
+
51
63
  function buildStatus(projectRoot) {
52
64
  const bus = readBus(projectRoot);
53
65
  const decisions = readDecisions(projectRoot);
54
66
  const unread = readUnread(projectRoot);
55
- const subscribers = bus ? Object.keys(bus.subscribers || {}) : [];
67
+ const subscribers = bus ? Object.keys(bus.agents || {}) : [];
68
+
56
69
  const activeEntries = bus
57
- ? Object.entries(bus.subscribers || {})
58
- .filter(([, meta]) => meta.status === "active")
70
+ ? Object.entries(bus.agents || {})
71
+ .filter(([, meta]) => isMetaActive(meta))
72
+ .filter(([id, meta]) => !isHiddenSubscriber(id, meta))
59
73
  .map(([id, meta]) => ({ id, meta }))
60
74
  : [];
61
75
  const active = activeEntries.map(({ id }) => id);
62
76
  const activeMeta = activeEntries.map(({ id, meta }) => {
63
77
  const nickname = meta?.nickname || "";
64
78
  const display = nickname ? nickname : id;
65
- return { id, nickname, display };
79
+ const launch_mode = meta?.launch_mode || "unknown";
80
+ const tmux_pane = meta?.tmux_pane || "";
81
+ const tty = meta?.tty || "";
82
+ return { id, nickname, display, launch_mode, tmux_pane, tty };
66
83
  });
67
84
 
68
85
  return {
@@ -0,0 +1,50 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const ContextDoctor = require("../context/doctor");
4
+
5
+ class RepoDoctor {
6
+ constructor(repoRoot) {
7
+ this.repoRoot = repoRoot;
8
+ this.failed = false;
9
+ }
10
+
11
+ fail(message) {
12
+ console.error(`FAIL: ${message}`);
13
+ this.failed = true;
14
+ }
15
+
16
+ run() {
17
+ const contextMod = path.join(this.repoRoot, "modules", "context");
18
+
19
+ const contextExists = fs.existsSync(contextMod);
20
+ if (!contextExists) {
21
+ this.fail(`missing ${contextMod}`);
22
+ }
23
+
24
+ if (contextExists) {
25
+ const contextDoctor = new ContextDoctor(this.repoRoot);
26
+ const ok = contextDoctor.lintProtocol();
27
+ if (!ok) this.failed = true;
28
+ }
29
+
30
+ console.log("=== ufoo doctor ===");
31
+ console.log(`Monorepo: ${this.repoRoot}`);
32
+ console.log("Modules:");
33
+ if (contextExists) {
34
+ console.log(`- context: ${contextMod}`);
35
+ }
36
+ const resources = path.join(this.repoRoot, "modules", "resources");
37
+ if (fs.existsSync(resources)) {
38
+ console.log(`- resources: ${resources}`);
39
+ }
40
+
41
+ if (this.failed) {
42
+ console.log("Status: FAILED");
43
+ return false;
44
+ }
45
+ console.log("Status: OK");
46
+ return true;
47
+ }
48
+ }
49
+
50
+ module.exports = RepoDoctor;