u-foo 1.2.13 → 1.2.14
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/bin/ufoo.js +149 -0
- package/modules/online/SKILLS/ufoo-online/SKILL.md +2 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +32 -14
- package/src/agent/notifier.js +5 -1
- package/src/agent/ufooAgent.js +1 -1
- package/src/bus/index.js +10 -3
- package/src/bus/inject.js +6 -3
- package/src/bus/subscriber.js +15 -2
- package/src/chat/commandExecutor.js +138 -10
- package/src/chat/commands.js +2 -1
- package/src/chat/daemonMessageRouter.js +40 -0
- package/src/chat/index.js +10 -29
- package/src/cli/onlineCoreCommands.js +11 -11
- package/src/cli.js +43 -4
- package/src/code/tui.js +53 -29
- package/src/daemon/cronOps.js +362 -29
- package/src/daemon/index.js +64 -0
- package/src/daemon/status.js +3 -0
- package/src/online/bridge.js +1 -1
- package/src/online/client.js +1 -1
- package/src/shared/eventContract.js +1 -0
package/bin/ufoo.js
CHANGED
|
@@ -42,6 +42,155 @@ async function main() {
|
|
|
42
42
|
await runChat(process.cwd());
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
// Handle resume command to resume/launch agent sessions
|
|
47
|
+
if (cmd === "resume") {
|
|
48
|
+
const target = process.argv[3];
|
|
49
|
+
if (!target) {
|
|
50
|
+
console.error("Error: resume requires an agent type or nickname");
|
|
51
|
+
console.error("Usage: ufoo resume <ucode|uclaude|ucodex|nickname>");
|
|
52
|
+
console.error("");
|
|
53
|
+
console.error("Examples:");
|
|
54
|
+
console.error(" ufoo resume ucode # Start new ucode agent");
|
|
55
|
+
console.error(" ufoo resume ucode-1 # Resume agent with nickname ucode-1");
|
|
56
|
+
console.error(" ufoo resume uclaude # Start new uclaude agent");
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { execSync } = require("child_process");
|
|
62
|
+
const path = require("path");
|
|
63
|
+
const { spawn } = require("child_process");
|
|
64
|
+
|
|
65
|
+
// First check if it's a nickname for an existing online agent
|
|
66
|
+
let targetAgent = null;
|
|
67
|
+
try {
|
|
68
|
+
const statusOutput = execSync("ufoo bus status", { encoding: "utf8", cwd: process.cwd() });
|
|
69
|
+
|
|
70
|
+
// Parse online agents
|
|
71
|
+
const onlineAgents = [];
|
|
72
|
+
const lines = statusOutput.split("\n");
|
|
73
|
+
let inOnlineSection = false;
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.includes("Online agents:")) {
|
|
77
|
+
inOnlineSection = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (inOnlineSection) {
|
|
81
|
+
const trimmedLine = line.trim();
|
|
82
|
+
if (!trimmedLine) continue;
|
|
83
|
+
if (trimmedLine.includes("Event statistics:") || trimmedLine.includes("===")) {
|
|
84
|
+
inOnlineSection = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Parse agent line: "ufoo-code:abc123 (nickname)"
|
|
88
|
+
const agentMatch = trimmedLine.match(/^[-\s]*([a-z-]+:[a-f0-9]+|[a-z-]+)(?:\s+\(([^)]+)\))?/i);
|
|
89
|
+
if (agentMatch) {
|
|
90
|
+
const subscriberId = agentMatch[1];
|
|
91
|
+
const nickname = agentMatch[2] || "";
|
|
92
|
+
onlineAgents.push({ subscriberId, nickname });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if target matches any online agent's nickname
|
|
98
|
+
for (const agent of onlineAgents) {
|
|
99
|
+
if (agent.nickname && agent.nickname === target) {
|
|
100
|
+
targetAgent = agent;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// Ignore errors from bus status check
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (targetAgent) {
|
|
109
|
+
// Found an online agent with this nickname
|
|
110
|
+
// Determine the agent type from subscriber ID
|
|
111
|
+
const subscriberId = targetAgent.subscriberId;
|
|
112
|
+
const [agentType, sessionId] = subscriberId.split(":");
|
|
113
|
+
|
|
114
|
+
let scriptName = "";
|
|
115
|
+
let displayName = "";
|
|
116
|
+
|
|
117
|
+
if (agentType === "ufoo-code") {
|
|
118
|
+
scriptName = "ucode.js";
|
|
119
|
+
displayName = "ucode";
|
|
120
|
+
} else if (agentType === "claude-code") {
|
|
121
|
+
scriptName = "uclaude.js";
|
|
122
|
+
displayName = "uclaude";
|
|
123
|
+
} else if (agentType === "codex") {
|
|
124
|
+
scriptName = "ucodex.js";
|
|
125
|
+
displayName = "ucodex";
|
|
126
|
+
} else {
|
|
127
|
+
console.error(`Error: Unable to determine agent type for ${subscriberId}`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`Resuming ${displayName} session for ${targetAgent.nickname} (${subscriberId})`);
|
|
133
|
+
|
|
134
|
+
// Set environment variable to indicate this is a resume
|
|
135
|
+
const scriptPath = path.join(__dirname, scriptName);
|
|
136
|
+
const env = { ...process.env };
|
|
137
|
+
|
|
138
|
+
// Pass the subscriber ID so the agent can reuse the same identity
|
|
139
|
+
env.UFOO_SUBSCRIBER_ID = subscriberId;
|
|
140
|
+
|
|
141
|
+
// Spawn the agent process - it will reuse the subscriber ID
|
|
142
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
143
|
+
stdio: "inherit",
|
|
144
|
+
cwd: process.cwd(),
|
|
145
|
+
env,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
child.on("exit", (code) => {
|
|
149
|
+
process.exit(code || 0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Not an existing online agent - check if it's an agent type to launch
|
|
156
|
+
const targetLower = target.toLowerCase();
|
|
157
|
+
let scriptName = "";
|
|
158
|
+
|
|
159
|
+
if (targetLower === "ucode" || targetLower === "ufoo-code" || targetLower === "ufoo") {
|
|
160
|
+
scriptName = "ucode.js";
|
|
161
|
+
} else if (targetLower === "uclaude" || targetLower === "claude-code" || targetLower === "claude") {
|
|
162
|
+
scriptName = "uclaude.js";
|
|
163
|
+
} else if (targetLower === "ucodex" || targetLower === "codex" || targetLower === "openai") {
|
|
164
|
+
scriptName = "ucodex.js";
|
|
165
|
+
} else {
|
|
166
|
+
// Not a valid agent type - might be an offline agent nickname
|
|
167
|
+
console.error(`Error: Agent '${target}' is not online and is not a valid agent type`);
|
|
168
|
+
console.error("");
|
|
169
|
+
console.error("Valid agent types: ucode, uclaude, ucodex");
|
|
170
|
+
console.error("");
|
|
171
|
+
console.error("To see online agents, run: ufoo bus status");
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Run the agent script directly for a new session
|
|
177
|
+
const scriptPath = path.join(__dirname, scriptName);
|
|
178
|
+
console.log(`Starting new ${target} session...`);
|
|
179
|
+
|
|
180
|
+
// Spawn the agent process and inherit stdio for interactive mode
|
|
181
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
182
|
+
stdio: "inherit",
|
|
183
|
+
cwd: process.cwd(),
|
|
184
|
+
env: process.env,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
child.on("exit", (code) => {
|
|
188
|
+
process.exit(code || 0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
45
194
|
await runCli(process.argv);
|
|
46
195
|
}
|
|
47
196
|
|
|
@@ -62,7 +62,7 @@ Inbox retention: channel messages 7 days, room messages 30 days.
|
|
|
62
62
|
## Full Connect Options
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
ufoo online connect --nickname <name> [--url <
|
|
65
|
+
ufoo online connect --nickname <name> [--url <wss://...>] [--subscriber <id>]
|
|
66
66
|
[--token <tok>] [--token-hash <hash>] [--world <name>] [--ping-ms <ms>]
|
|
67
67
|
[--join <channel>] [--room <room-id> --room-password <pwd>]
|
|
68
68
|
[--interval <ms>] [--allow-insecure-ws]
|
|
@@ -131,7 +131,7 @@ ufoo online inbox agent-b # See agent-a's message
|
|
|
131
131
|
### 2. Private room collaboration
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
|
-
ufoo online room create --type private --password secret --server
|
|
134
|
+
ufoo online room create --type private --password secret --server https://online.ufoo.dev
|
|
135
135
|
# → returns room_id
|
|
136
136
|
|
|
137
137
|
ufoo online connect --nickname dev-1 --room room_001 --room-password secret
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -74,31 +74,49 @@ function forceSymlink(target, linkPath) {
|
|
|
74
74
|
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
75
75
|
}
|
|
76
76
|
} catch {
|
|
77
|
-
// doesn't exist
|
|
77
|
+
// doesn't exist - fine
|
|
78
78
|
}
|
|
79
79
|
fs.symlinkSync(target, linkPath);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
function installClaudeCommands(home, sources) {
|
|
83
|
+
const commandsDir = path.join(home, ".claude", "commands");
|
|
84
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
85
|
+
|
|
86
|
+
for (const { name, md } of sources) {
|
|
87
|
+
forceSymlink(md, path.join(commandsDir, `${name}.md`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`[postinstall] Installed ${sources.length} ufoo command(s) to ${commandsDir}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function installSkillDirs(targetDir, sources, label) {
|
|
94
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
for (const { name, dir } of sources) {
|
|
97
|
+
forceSymlink(dir, path.join(targetDir, name));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`[postinstall] Installed ${sources.length} ufoo skill(s) to ${label}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Install ufoo skills for Claude and Codex at npm install time.
|
|
104
|
+
// - Claude slash commands: ~/.claude/commands/<name>.md -> SKILL.md
|
|
105
|
+
// - Claude skills: ~/.claude/skills/<name> -> skill dir
|
|
106
|
+
// - Codex skills: ${CODEX_HOME:-~/.codex}/skills/<name> -> skill dir
|
|
84
107
|
try {
|
|
85
108
|
const pkgRoot = path.resolve(__dirname, "..");
|
|
86
109
|
const home = os.homedir();
|
|
87
110
|
const sources = collectSkillSources(pkgRoot);
|
|
88
111
|
|
|
89
112
|
if (sources.length > 0) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for (const { name, md } of sources) {
|
|
96
|
-
forceSymlink(md, path.join(commandsDir, `${name}.md`));
|
|
97
|
-
installed += 1;
|
|
98
|
-
}
|
|
99
|
-
console.log(`[postinstall] Installed ${installed} ufoo command(s) to ${commandsDir}`);
|
|
113
|
+
installClaudeCommands(home, sources);
|
|
114
|
+
installSkillDirs(path.join(home, ".claude", "skills"), sources, "~/.claude/skills");
|
|
115
|
+
|
|
116
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
117
|
+
installSkillDirs(path.join(codexHome, "skills"), sources, `${codexHome}/skills`);
|
|
100
118
|
}
|
|
101
119
|
} catch (err) {
|
|
102
|
-
// Non-fatal
|
|
120
|
+
// Non-fatal - skills can be installed manually via `ufoo skills install`
|
|
103
121
|
console.log(`[postinstall] Skipped skills install: ${err.message}`);
|
|
104
122
|
}
|
package/src/agent/notifier.js
CHANGED
|
@@ -175,7 +175,11 @@ class AgentNotifier {
|
|
|
175
175
|
data.message = `delivery failed to ${this.lastNickname || this.subscriber}: ${errorMessage || "unknown error"}`;
|
|
176
176
|
}
|
|
177
177
|
try {
|
|
178
|
-
await this.eventBus.send(publisher, "", this.subscriber, {
|
|
178
|
+
await this.eventBus.send(publisher, "", this.subscriber, {
|
|
179
|
+
event: "delivery",
|
|
180
|
+
data,
|
|
181
|
+
silent: true,
|
|
182
|
+
});
|
|
179
183
|
} catch {
|
|
180
184
|
// ignore delivery emit failures
|
|
181
185
|
}
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -99,7 +99,7 @@ function buildSystemPrompt(context) {
|
|
|
99
99
|
"- If multiple possible agents, use disambiguate with candidates and no dispatch.",
|
|
100
100
|
"- If user specifies a nickname for a new agent, include ops.launch with nickname so daemon can rename.",
|
|
101
101
|
"- If user requests rename, use ops.rename with agent_id and nickname (do NOT launch).",
|
|
102
|
-
"- For scheduled follow-up (cron
|
|
102
|
+
"- For scheduled follow-up (cron), use ops.cron with operation=start and include every+target(s)+prompt (or at for one-time).",
|
|
103
103
|
"- To check scheduled tasks, use ops.cron with operation=list.",
|
|
104
104
|
"- To stop scheduled tasks, use ops.cron with operation=stop and id (or id=all).",
|
|
105
105
|
"- Use top-level assistant_call for project exploration, temporary shell tasks, and quick execution support.",
|
package/src/bus/index.js
CHANGED
|
@@ -308,9 +308,16 @@ class EventBus {
|
|
|
308
308
|
const result = eventName === "message"
|
|
309
309
|
? await this.messageManager.send(target, message, publisher)
|
|
310
310
|
: await this.messageManager.emit(target, eventName, data, publisher);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
const silent = options.silent === true;
|
|
312
|
+
if (!silent && eventName === "message") {
|
|
313
|
+
logOk(
|
|
314
|
+
`Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
|
|
315
|
+
);
|
|
316
|
+
} else if (!silent && process.env.UFOO_BUS_VERBOSE_EVENTS === "1") {
|
|
317
|
+
logInfo(
|
|
318
|
+
`Event sent: event=${eventName} seq=${result.seq} -> ${result.targets.join(", ")}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
314
321
|
return result;
|
|
315
322
|
} catch (err) {
|
|
316
323
|
logError(err.message);
|
package/src/bus/inject.js
CHANGED
|
@@ -284,8 +284,11 @@ class Injector {
|
|
|
284
284
|
// 读取 tty(tmux 需要)
|
|
285
285
|
const tty = this.readTty(subscriber);
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
// 2. 尝试 tmux(无需权限)
|
|
288
|
+
// Launch mode may be temporarily missing/stale (e.g. rejoin from non-interactive context).
|
|
289
|
+
// In that case still try tmux fallback by pane/tty.
|
|
290
|
+
const allowTmuxFallback = supportsNotifier || !launchMode || launchMode === "terminal" || launchMode === "tmux";
|
|
291
|
+
if (allowTmuxFallback) {
|
|
289
292
|
const tmuxPane = meta.tmux_pane || this.getTmuxPane(subscriber);
|
|
290
293
|
if (tmuxPane) {
|
|
291
294
|
const paneExists = await this.checkTmuxPane(tmuxPane);
|
|
@@ -296,7 +299,7 @@ class Injector {
|
|
|
296
299
|
}
|
|
297
300
|
}
|
|
298
301
|
|
|
299
|
-
//
|
|
302
|
+
// Try resolving pane via tty when tmux pane metadata is missing.
|
|
300
303
|
if (tty && isValidTty(tty)) {
|
|
301
304
|
const fallbackPane = await this.findTmuxPaneByTty(tty);
|
|
302
305
|
if (fallbackPane) {
|
package/src/bus/subscriber.js
CHANGED
|
@@ -161,7 +161,15 @@ class SubscriberManager {
|
|
|
161
161
|
nicknameManager.setNickname(subscriber, finalNickname);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const
|
|
164
|
+
const explicitLaunchMode = typeof options.launchMode === "string"
|
|
165
|
+
? options.launchMode.trim()
|
|
166
|
+
: "";
|
|
167
|
+
const envLaunchMode = typeof process.env.UFOO_LAUNCH_MODE === "string"
|
|
168
|
+
? process.env.UFOO_LAUNCH_MODE.trim()
|
|
169
|
+
: "";
|
|
170
|
+
const preservedLaunchMode = existingMeta?.launch_mode || "";
|
|
171
|
+
const inferredLaunchMode = process.env.TMUX_PANE ? "tmux" : "";
|
|
172
|
+
const launchMode = explicitLaunchMode || envLaunchMode || preservedLaunchMode || inferredLaunchMode;
|
|
165
173
|
const overridePid = Number.isFinite(options.parentPid) && options.parentPid > 0
|
|
166
174
|
? options.parentPid
|
|
167
175
|
: null;
|
|
@@ -184,6 +192,11 @@ class SubscriberManager {
|
|
|
184
192
|
const preserved = existingMeta && typeof existingMeta === "object"
|
|
185
193
|
? { ...existingMeta }
|
|
186
194
|
: {};
|
|
195
|
+
const explicitTmuxPane = typeof options.tmuxPane === "string" ? options.tmuxPane.trim() : "";
|
|
196
|
+
const envTmuxPane = typeof process.env.TMUX_PANE === "string" ? process.env.TMUX_PANE.trim() : "";
|
|
197
|
+
const preservedTmuxPane = typeof existingMeta?.tmux_pane === "string" ? existingMeta.tmux_pane.trim() : "";
|
|
198
|
+
const tmuxPane = explicitTmuxPane || envTmuxPane || preservedTmuxPane;
|
|
199
|
+
|
|
187
200
|
this.busData.agents[subscriber] = {
|
|
188
201
|
...preserved,
|
|
189
202
|
agent_type: agentType,
|
|
@@ -194,7 +207,7 @@ class SubscriberManager {
|
|
|
194
207
|
pid: overridePid || getJoinedPid(),
|
|
195
208
|
tty: finalTty,
|
|
196
209
|
tty_shell_pid: ttyInfo?.shellPid || 0,
|
|
197
|
-
tmux_pane:
|
|
210
|
+
tmux_pane: tmuxPane,
|
|
198
211
|
launch_mode: launchMode,
|
|
199
212
|
};
|
|
200
213
|
|
|
@@ -69,6 +69,7 @@ function createCommandExecutor(options = {}) {
|
|
|
69
69
|
createCronTask = () => null,
|
|
70
70
|
listCronTasks = () => [],
|
|
71
71
|
stopCronTask = () => false,
|
|
72
|
+
requestCron = null,
|
|
72
73
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
73
74
|
schedule = (fn, ms) => setTimeout(fn, ms),
|
|
74
75
|
} = options;
|
|
@@ -419,9 +420,44 @@ function createCommandExecutor(options = {}) {
|
|
|
419
420
|
.filter(Boolean);
|
|
420
421
|
}
|
|
421
422
|
|
|
422
|
-
|
|
423
|
+
function parseCronAtMs(raw = "") {
|
|
424
|
+
const text = String(raw || "").trim();
|
|
425
|
+
if (!text) return 0;
|
|
426
|
+
|
|
427
|
+
if (/^\d+$/.test(text)) {
|
|
428
|
+
const value = Number.parseInt(text, 10);
|
|
429
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
430
|
+
return text.length <= 10 ? value * 1000 : value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const normalized = text.replace(/\//g, "-");
|
|
434
|
+
const directMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})(?::(\d{2}))?$/);
|
|
435
|
+
if (directMatch) {
|
|
436
|
+
const seconds = directMatch[3] || "00";
|
|
437
|
+
const parsed = Date.parse(`${directMatch[1]}T${directMatch[2]}:${seconds}`);
|
|
438
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const parsed = Date.parse(normalized);
|
|
442
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function formatCronAt(ms = 0) {
|
|
446
|
+
const ts = Number(ms) || 0;
|
|
447
|
+
if (ts <= 0) return "";
|
|
448
|
+
const d = new Date(ts);
|
|
449
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
450
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function handleCronCommand(args = []) {
|
|
423
454
|
const action = String(args[0] || "").trim().toLowerCase();
|
|
424
455
|
if (action === "list" || action === "ls") {
|
|
456
|
+
if (typeof requestCron === "function") {
|
|
457
|
+
requestCron({ operation: "list" });
|
|
458
|
+
schedule(requestStatus, 200);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
425
461
|
const tasks = Array.isArray(listCronTasks()) ? listCronTasks() : [];
|
|
426
462
|
if (tasks.length === 0) {
|
|
427
463
|
logMessage("system", "{cyan-fg}Cron:{/cyan-fg} none");
|
|
@@ -437,7 +473,12 @@ function createCommandExecutor(options = {}) {
|
|
|
437
473
|
if (action === "stop" || action === "rm" || action === "remove") {
|
|
438
474
|
const target = String(args[1] || "").trim();
|
|
439
475
|
if (!target) {
|
|
440
|
-
logMessage("error", "{white-fg}✗{/white-fg} Usage: /
|
|
476
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /cron stop <id|all>");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (typeof requestCron === "function") {
|
|
480
|
+
requestCron({ operation: "stop", id: target });
|
|
481
|
+
schedule(requestStatus, 200);
|
|
441
482
|
return;
|
|
442
483
|
}
|
|
443
484
|
if (target === "all") {
|
|
@@ -464,6 +505,16 @@ function createCommandExecutor(options = {}) {
|
|
|
464
505
|
const intervalRaw = String(
|
|
465
506
|
kv.every || kv.interval || kv.interval_ms || kv.ms || ""
|
|
466
507
|
).trim();
|
|
508
|
+
const atRaw = String(
|
|
509
|
+
kv.at ||
|
|
510
|
+
kv.once ||
|
|
511
|
+
kv.run_at ||
|
|
512
|
+
kv.runat ||
|
|
513
|
+
kv.datetime ||
|
|
514
|
+
kv.date_time ||
|
|
515
|
+
((kv.date && kv.time) ? `${kv.date} ${kv.time}` : "") ||
|
|
516
|
+
""
|
|
517
|
+
).trim();
|
|
467
518
|
const targetsRaw = String(
|
|
468
519
|
kv.target || kv.targets || kv.agent || kv.agents || ""
|
|
469
520
|
).trim();
|
|
@@ -471,26 +522,66 @@ function createCommandExecutor(options = {}) {
|
|
|
471
522
|
kv.prompt || kv.message || kv.msg || nonKvParts.join(" ") || ""
|
|
472
523
|
).trim();
|
|
473
524
|
|
|
474
|
-
if (!intervalRaw || !targetsRaw || !prompt) {
|
|
525
|
+
if ((!intervalRaw && !atRaw) || !targetsRaw || !prompt) {
|
|
475
526
|
logMessage(
|
|
476
527
|
"error",
|
|
477
|
-
"{white-fg}✗{/white-fg} Usage: /
|
|
528
|
+
"{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> prompt=\"...\""
|
|
478
529
|
);
|
|
479
530
|
return;
|
|
480
531
|
}
|
|
481
532
|
|
|
482
|
-
|
|
483
|
-
|
|
533
|
+
if (intervalRaw && atRaw) {
|
|
534
|
+
logMessage("error", "{white-fg}✗{/white-fg} Use either every=... or at=..., not both");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const intervalMs = intervalRaw ? parseIntervalMs(intervalRaw) : 0;
|
|
539
|
+
if (intervalRaw && (!Number.isFinite(intervalMs) || intervalMs < 1000)) {
|
|
484
540
|
logMessage("error", "{white-fg}✗{/white-fg} Invalid interval (min 1s)");
|
|
485
541
|
return;
|
|
486
542
|
}
|
|
487
543
|
|
|
544
|
+
const atMs = atRaw ? parseCronAtMs(atRaw) : 0;
|
|
545
|
+
if (atRaw && (!Number.isFinite(atMs) || atMs <= 0)) {
|
|
546
|
+
logMessage("error", "{white-fg}✗{/white-fg} Invalid one-time schedule, use at=\"YYYY-MM-DD HH:mm\"");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (atMs > 0 && atMs <= Date.now()) {
|
|
550
|
+
logMessage("error", "{white-fg}✗{/white-fg} One-time schedule must be in the future");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
488
554
|
const targets = parseCronTargets(targetsRaw);
|
|
489
555
|
if (targets.length === 0) {
|
|
490
556
|
logMessage("error", "{white-fg}✗{/white-fg} At least one target agent is required");
|
|
491
557
|
return;
|
|
492
558
|
}
|
|
493
559
|
|
|
560
|
+
if (typeof requestCron === "function") {
|
|
561
|
+
if (atMs > 0) {
|
|
562
|
+
requestCron({
|
|
563
|
+
operation: "start",
|
|
564
|
+
once_at_ms: atMs,
|
|
565
|
+
targets,
|
|
566
|
+
prompt,
|
|
567
|
+
});
|
|
568
|
+
} else {
|
|
569
|
+
requestCron({
|
|
570
|
+
operation: "start",
|
|
571
|
+
interval_ms: intervalMs,
|
|
572
|
+
targets,
|
|
573
|
+
prompt,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
schedule(requestStatus, 200);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (atMs > 0) {
|
|
581
|
+
logMessage("error", "{white-fg}✗{/white-fg} One-time cron requires daemon-backed scheduler");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
494
585
|
const task = createCronTask({
|
|
495
586
|
intervalMs,
|
|
496
587
|
targets,
|
|
@@ -503,7 +594,7 @@ function createCommandExecutor(options = {}) {
|
|
|
503
594
|
|
|
504
595
|
logMessage(
|
|
505
596
|
"system",
|
|
506
|
-
`{white-fg}✓{/white-fg} Cron started ${task.id}: every ${formatIntervalMs(intervalMs)} -> ${targets.join(", ")}`
|
|
597
|
+
`{white-fg}✓{/white-fg} Cron started ${task.id}: ${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`
|
|
507
598
|
);
|
|
508
599
|
}
|
|
509
600
|
|
|
@@ -554,6 +645,39 @@ function createCommandExecutor(options = {}) {
|
|
|
554
645
|
});
|
|
555
646
|
}
|
|
556
647
|
|
|
648
|
+
async function handleUfooCommand(args = []) {
|
|
649
|
+
// Handle /ufoo command (session marker from daemon)
|
|
650
|
+
// When daemon sends /ufoo <marker>, we should just check for pending messages
|
|
651
|
+
if (args.length > 0) {
|
|
652
|
+
// This is a probe marker, check for pending messages
|
|
653
|
+
const subscriberId = process.env.UFOO_SUBSCRIBER_ID;
|
|
654
|
+
if (subscriberId) {
|
|
655
|
+
try {
|
|
656
|
+
const bus = createBus(projectRoot);
|
|
657
|
+
bus.ensureBus();
|
|
658
|
+
const pendingMessages = bus.checkMessages(subscriberId);
|
|
659
|
+
if (pendingMessages && pendingMessages.length > 0) {
|
|
660
|
+
logMessage("system", `{cyan-fg}[bus]{/cyan-fg} ${pendingMessages.length} pending message(s)`);
|
|
661
|
+
}
|
|
662
|
+
} catch {
|
|
663
|
+
// Ignore errors when checking messages
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Don't log anything else for probe markers
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Without arguments, show ufoo protocol documentation
|
|
671
|
+
logMessage("system", "{cyan-fg}ufoo Protocol{/cyan-fg}");
|
|
672
|
+
logMessage("system", "");
|
|
673
|
+
logMessage("system", "This project uses ufoo for agent coordination:");
|
|
674
|
+
logMessage("system", " • Context decisions: /ctx");
|
|
675
|
+
logMessage("system", " • Event bus: /bus");
|
|
676
|
+
logMessage("system", " • Initialize: /init");
|
|
677
|
+
logMessage("system", "");
|
|
678
|
+
logMessage("system", "For detailed documentation, see .ufoo/docs/");
|
|
679
|
+
}
|
|
680
|
+
|
|
557
681
|
async function handleUcodeConfigCommand(args = []) {
|
|
558
682
|
const first = String(args[0] || "").trim().toLowerCase();
|
|
559
683
|
const hasInlineKv = args.some((item) => String(item || "").includes("="));
|
|
@@ -668,12 +792,15 @@ function createCommandExecutor(options = {}) {
|
|
|
668
792
|
case "resume":
|
|
669
793
|
await handleResumeCommand(args);
|
|
670
794
|
return true;
|
|
671
|
-
case "
|
|
672
|
-
await
|
|
795
|
+
case "cron":
|
|
796
|
+
await handleCronCommand(args);
|
|
673
797
|
return true;
|
|
674
798
|
case "settings":
|
|
675
799
|
await handleSettingsCommand(args);
|
|
676
800
|
return true;
|
|
801
|
+
case "ufoo":
|
|
802
|
+
await handleUfooCommand(args);
|
|
803
|
+
return true;
|
|
677
804
|
default:
|
|
678
805
|
logMessage("error", `{white-fg}✗{/white-fg} Unknown command: /${command}`);
|
|
679
806
|
return true;
|
|
@@ -691,9 +818,10 @@ function createCommandExecutor(options = {}) {
|
|
|
691
818
|
handleSkillsCommand,
|
|
692
819
|
handleLaunchCommand,
|
|
693
820
|
handleResumeCommand,
|
|
694
|
-
|
|
821
|
+
handleCronCommand,
|
|
695
822
|
handleSettingsCommand,
|
|
696
823
|
handleUcodeConfigCommand,
|
|
824
|
+
handleUfooCommand,
|
|
697
825
|
};
|
|
698
826
|
}
|
|
699
827
|
|
package/src/chat/commands.js
CHANGED
|
@@ -27,7 +27,7 @@ const COMMAND_TREE = {
|
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
"/doctor": { desc: "Health check diagnostics" },
|
|
30
|
-
"/
|
|
30
|
+
"/cron": {
|
|
31
31
|
desc: "Cron scheduler operations",
|
|
32
32
|
children: {
|
|
33
33
|
start: { desc: "Create cron task" },
|
|
@@ -64,6 +64,7 @@ const COMMAND_TREE = {
|
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
"/status": { desc: "Status display" },
|
|
67
|
+
"/ufoo": { desc: "ufoo protocol (session marker)" },
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const COMMAND_ORDER = ["/launch", "/bus", "/ctx"];
|
|
@@ -110,6 +110,46 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
if (payload.cron && typeof payload.cron === "object") {
|
|
114
|
+
const cron = payload.cron;
|
|
115
|
+
const operation = String(cron.operation || "").toLowerCase();
|
|
116
|
+
if (!cron.ok) {
|
|
117
|
+
logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(cron.error || "cron failed")}`);
|
|
118
|
+
} else if (operation === "list" || operation === "ls") {
|
|
119
|
+
const tasks = Array.isArray(cron.tasks) ? cron.tasks : [];
|
|
120
|
+
if (tasks.length === 0) {
|
|
121
|
+
logMessage("system", "{cyan-fg}Cron:{/cyan-fg} none");
|
|
122
|
+
} else {
|
|
123
|
+
logMessage("system", `{cyan-fg}Cron:{/cyan-fg} ${tasks.length} task(s)`);
|
|
124
|
+
tasks.forEach((task) => {
|
|
125
|
+
const summary = task && (task.summary || task.id) ? (task.summary || task.id) : "";
|
|
126
|
+
if (summary) {
|
|
127
|
+
logMessage("system", ` • ${escapeBlessed(summary)}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
} else if (operation === "start" && cron.task) {
|
|
132
|
+
const task = cron.task;
|
|
133
|
+
if (task.mode === "once") {
|
|
134
|
+
logMessage(
|
|
135
|
+
"system",
|
|
136
|
+
`{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)} at ${escapeBlessed(task.onceAt || String(task.onceAtMs || ""))}`
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
logMessage(
|
|
140
|
+
"system",
|
|
141
|
+
`{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: every ${escapeBlessed(task.interval || String(task.intervalMs || ""))}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
} else if (operation === "stop") {
|
|
145
|
+
if (cron.id === "all") {
|
|
146
|
+
logMessage("system", `{white-fg}✓{/white-fg} Stopped ${Number(cron.stopped) || 0} cron task(s)`);
|
|
147
|
+
} else if (cron.id) {
|
|
148
|
+
logMessage("system", `{white-fg}✓{/white-fg} Stopped cron task ${escapeBlessed(cron.id)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
if (payload.dispatch && payload.dispatch.length > 0) {
|
|
114
154
|
const targets = payload.dispatch.map((d) => d.target || d).join(", ");
|
|
115
155
|
logMessage("dispatch", `{white-fg}→{/white-fg} Dispatched to: ${escapeBlessed(targets)}`);
|