u-foo 1.0.6 → 1.1.9
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 +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
package/src/config.js
CHANGED
|
@@ -5,6 +5,14 @@ const DEFAULT_CONFIG = {
|
|
|
5
5
|
launchMode: "auto",
|
|
6
6
|
agentProvider: "codex-cli",
|
|
7
7
|
agentModel: "",
|
|
8
|
+
assistantEngine: "auto",
|
|
9
|
+
assistantModel: "",
|
|
10
|
+
assistantUfooCmd: "",
|
|
11
|
+
ucodeProvider: "",
|
|
12
|
+
ucodeModel: "",
|
|
13
|
+
ucodeBaseUrl: "",
|
|
14
|
+
ucodeApiKey: "",
|
|
15
|
+
ucodeAgentDir: "",
|
|
8
16
|
autoResume: false,
|
|
9
17
|
};
|
|
10
18
|
|
|
@@ -20,6 +28,15 @@ function normalizeAgentProvider(value) {
|
|
|
20
28
|
return value === "claude-cli" ? "claude-cli" : "codex-cli";
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
function normalizeAssistantEngine(value) {
|
|
32
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
33
|
+
if (!raw || raw === "auto") return "auto";
|
|
34
|
+
if (raw === "codex" || raw === "codex-cli" || raw === "codex-code") return "codex";
|
|
35
|
+
if (raw === "claude" || raw === "claude-cli" || raw === "claude-code") return "claude";
|
|
36
|
+
if (raw === "ufoo") return "ufoo";
|
|
37
|
+
return "auto";
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
function configPath(projectRoot) {
|
|
24
41
|
return path.join(projectRoot, ".ufoo", "config.json");
|
|
25
42
|
}
|
|
@@ -32,6 +49,14 @@ function loadConfig(projectRoot) {
|
|
|
32
49
|
...raw,
|
|
33
50
|
launchMode: normalizeLaunchMode(raw.launchMode),
|
|
34
51
|
agentProvider: normalizeAgentProvider(raw.agentProvider),
|
|
52
|
+
assistantEngine: normalizeAssistantEngine(raw.assistantEngine),
|
|
53
|
+
assistantModel: typeof raw.assistantModel === "string" ? raw.assistantModel : "",
|
|
54
|
+
assistantUfooCmd: typeof raw.assistantUfooCmd === "string" ? raw.assistantUfooCmd : "",
|
|
55
|
+
ucodeProvider: typeof raw.ucodeProvider === "string" ? raw.ucodeProvider : "",
|
|
56
|
+
ucodeModel: typeof raw.ucodeModel === "string" ? raw.ucodeModel : "",
|
|
57
|
+
ucodeBaseUrl: typeof raw.ucodeBaseUrl === "string" ? raw.ucodeBaseUrl : "",
|
|
58
|
+
ucodeApiKey: typeof raw.ucodeApiKey === "string" ? raw.ucodeApiKey : "",
|
|
59
|
+
ucodeAgentDir: typeof raw.ucodeAgentDir === "string" ? raw.ucodeAgentDir : "",
|
|
35
60
|
autoResume: raw.autoResume !== false,
|
|
36
61
|
};
|
|
37
62
|
} catch {
|
|
@@ -42,15 +67,36 @@ function loadConfig(projectRoot) {
|
|
|
42
67
|
function saveConfig(projectRoot, config) {
|
|
43
68
|
const target = configPath(projectRoot);
|
|
44
69
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
70
|
+
let existing = {};
|
|
71
|
+
try {
|
|
72
|
+
existing = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
existing = {};
|
|
75
|
+
}
|
|
45
76
|
const merged = {
|
|
46
77
|
...DEFAULT_CONFIG,
|
|
78
|
+
...existing,
|
|
47
79
|
...config,
|
|
48
80
|
};
|
|
49
81
|
merged.launchMode = normalizeLaunchMode(merged.launchMode);
|
|
50
82
|
merged.agentProvider = normalizeAgentProvider(merged.agentProvider);
|
|
83
|
+
merged.assistantEngine = normalizeAssistantEngine(merged.assistantEngine);
|
|
84
|
+
merged.assistantModel = typeof merged.assistantModel === "string" ? merged.assistantModel : "";
|
|
85
|
+
merged.assistantUfooCmd = typeof merged.assistantUfooCmd === "string" ? merged.assistantUfooCmd : "";
|
|
86
|
+
merged.ucodeProvider = typeof merged.ucodeProvider === "string" ? merged.ucodeProvider : "";
|
|
87
|
+
merged.ucodeModel = typeof merged.ucodeModel === "string" ? merged.ucodeModel : "";
|
|
88
|
+
merged.ucodeBaseUrl = typeof merged.ucodeBaseUrl === "string" ? merged.ucodeBaseUrl : "";
|
|
89
|
+
merged.ucodeApiKey = typeof merged.ucodeApiKey === "string" ? merged.ucodeApiKey : "";
|
|
90
|
+
merged.ucodeAgentDir = typeof merged.ucodeAgentDir === "string" ? merged.ucodeAgentDir : "";
|
|
51
91
|
merged.autoResume = merged.autoResume !== false;
|
|
52
92
|
fs.writeFileSync(target, JSON.stringify(merged, null, 2));
|
|
53
93
|
return merged;
|
|
54
94
|
}
|
|
55
95
|
|
|
56
|
-
module.exports = {
|
|
96
|
+
module.exports = {
|
|
97
|
+
loadConfig,
|
|
98
|
+
saveConfig,
|
|
99
|
+
normalizeLaunchMode,
|
|
100
|
+
normalizeAgentProvider,
|
|
101
|
+
normalizeAssistantEngine,
|
|
102
|
+
};
|
package/src/context/decisions.js
CHANGED
|
@@ -130,24 +130,34 @@ class DecisionsManager {
|
|
|
130
130
|
process.env.USERNAME ||
|
|
131
131
|
"unknown";
|
|
132
132
|
|
|
133
|
+
const nicknameRaw =
|
|
134
|
+
options.nickname ||
|
|
135
|
+
process.env.UFOO_NICKNAME ||
|
|
136
|
+
process.env.USER ||
|
|
137
|
+
process.env.USERNAME ||
|
|
138
|
+
"unknown";
|
|
139
|
+
|
|
133
140
|
const status = options.status || "open";
|
|
134
141
|
const num = this.nextNumber();
|
|
135
142
|
const slug = this.slugify(title);
|
|
143
|
+
const nick = this.slugify(nicknameRaw);
|
|
136
144
|
|
|
137
145
|
fs.mkdirSync(this.contextDir, { recursive: true });
|
|
138
146
|
fs.mkdirSync(this.decisionsDir, { recursive: true });
|
|
139
147
|
|
|
140
|
-
const file = `${num}-${slug}.md`;
|
|
148
|
+
const file = `${num}-${nick}-${slug}.md`;
|
|
141
149
|
const filePath = path.join(this.decisionsDir, file);
|
|
142
150
|
const date = new Date().toISOString().slice(0, 10);
|
|
143
151
|
|
|
144
152
|
const content =
|
|
145
153
|
`---\n` +
|
|
146
154
|
`status: ${status}\n` +
|
|
155
|
+
`nickname: ${nicknameRaw}\n` +
|
|
147
156
|
`---\n` +
|
|
148
157
|
`# DECISION ${num}: ${title}\n\n` +
|
|
149
158
|
`Date: ${date}\n` +
|
|
150
|
-
`Author: ${author}\n
|
|
159
|
+
`Author: ${author}\n` +
|
|
160
|
+
`Nickname: ${nicknameRaw}\n\n` +
|
|
151
161
|
`Context:\nWhat led to this decision?\n\n` +
|
|
152
162
|
`Decision:\nWhat is now considered true?\n\n` +
|
|
153
163
|
`Implications:\nWhat must follow from this?\n`;
|
package/src/context/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const ContextDoctor = require("./doctor");
|
|
2
2
|
const DecisionsManager = require("./decisions");
|
|
3
|
+
const SyncManager = require("./sync");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Context management wrapper for chat commands
|
|
@@ -9,6 +10,7 @@ class UfooContext {
|
|
|
9
10
|
this.projectRoot = projectRoot;
|
|
10
11
|
this.doctorInstance = new ContextDoctor(projectRoot);
|
|
11
12
|
this.decisionsManager = new DecisionsManager(projectRoot);
|
|
13
|
+
this.syncManager = new SyncManager(projectRoot);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -31,7 +33,22 @@ class UfooContext {
|
|
|
31
33
|
async status() {
|
|
32
34
|
const decisions = this.decisionsManager.readDecisions();
|
|
33
35
|
const openDecisions = decisions.filter(d => d.status === "open");
|
|
34
|
-
|
|
36
|
+
const sync = this.syncManager.parseLines();
|
|
37
|
+
console.log(`Context: ${openDecisions.length} open decision(s), ${decisions.length} total, ${sync.length} sync note(s)`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Append a sync note
|
|
42
|
+
*/
|
|
43
|
+
async syncWrite(options = {}) {
|
|
44
|
+
return this.syncManager.write(options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Show sync notes
|
|
49
|
+
*/
|
|
50
|
+
async listSync(options = {}) {
|
|
51
|
+
return this.syncManager.list(options);
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sync log manager
|
|
6
|
+
* Stores lightweight agent progress notes as JSONL.
|
|
7
|
+
*/
|
|
8
|
+
class SyncManager {
|
|
9
|
+
constructor(projectRoot) {
|
|
10
|
+
this.projectRoot = projectRoot;
|
|
11
|
+
this.contextDir = path.join(projectRoot, ".ufoo", "context");
|
|
12
|
+
this.syncFile = path.join(this.contextDir, "sync.jsonl");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ensureContextDir() {
|
|
16
|
+
fs.mkdirSync(this.contextDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
parseLines() {
|
|
20
|
+
if (!fs.existsSync(this.syncFile)) return [];
|
|
21
|
+
const raw = fs.readFileSync(this.syncFile, "utf8");
|
|
22
|
+
if (!raw.trim()) return [];
|
|
23
|
+
const lines = raw
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
try {
|
|
31
|
+
entries.push(JSON.parse(line));
|
|
32
|
+
} catch {
|
|
33
|
+
// Skip malformed legacy lines.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return entries;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
normalizeActor(value, fallback = "unknown") {
|
|
40
|
+
const text = String(value || "").trim();
|
|
41
|
+
if (text) return text;
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildEntry(options = {}) {
|
|
46
|
+
const message = String(options.message || "").trim();
|
|
47
|
+
if (!message) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Missing sync message. Usage: ufoo ctx sync write [--for <agent>] \"message\""
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ts: new Date().toISOString(),
|
|
55
|
+
type: "sync",
|
|
56
|
+
from: this.normalizeActor(
|
|
57
|
+
options.from,
|
|
58
|
+
process.env.UFOO_SUBSCRIBER_ID ||
|
|
59
|
+
process.env.UFOO_NICKNAME ||
|
|
60
|
+
process.env.USER ||
|
|
61
|
+
process.env.USERNAME ||
|
|
62
|
+
"unknown"
|
|
63
|
+
),
|
|
64
|
+
for: this.normalizeActor(options.for, ""),
|
|
65
|
+
message,
|
|
66
|
+
decision: String(options.decision || "").trim(),
|
|
67
|
+
file: String(options.file || "").trim(),
|
|
68
|
+
tests: String(options.tests || "").trim(),
|
|
69
|
+
verification: String(options.verification || "").trim(),
|
|
70
|
+
risk: String(options.risk || "").trim(),
|
|
71
|
+
next: String(options.next || "").trim(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
write(options = {}) {
|
|
76
|
+
const entry = this.buildEntry(options);
|
|
77
|
+
this.ensureContextDir();
|
|
78
|
+
fs.appendFileSync(this.syncFile, `${JSON.stringify(entry)}\n`, "utf8");
|
|
79
|
+
console.log(this.formatEntry(entry));
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
formatEntry(entry) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
parts.push("[sync]");
|
|
86
|
+
if (entry.for) parts.push(`[for ${entry.for}]`);
|
|
87
|
+
if (entry.from) parts.push(`[from ${entry.from}]`);
|
|
88
|
+
parts.push(entry.message);
|
|
89
|
+
|
|
90
|
+
if (entry.decision) parts.push(`decision: ${entry.decision}.`);
|
|
91
|
+
if (entry.file) parts.push(`file: ${entry.file}.`);
|
|
92
|
+
if (entry.tests) parts.push(`tests: ${entry.tests}.`);
|
|
93
|
+
if (entry.verification) parts.push(`verification: ${entry.verification}.`);
|
|
94
|
+
if (entry.risk) parts.push(`risk: ${entry.risk}.`);
|
|
95
|
+
if (entry.next) parts.push(`next-cut: ${entry.next}.`);
|
|
96
|
+
|
|
97
|
+
return parts.join(" ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
list(options = {}) {
|
|
101
|
+
const num = Number.isFinite(options.num) && options.num > 0 ? options.num : 20;
|
|
102
|
+
const filterFor = String(options.for || "").trim();
|
|
103
|
+
const filterFrom = String(options.from || "").trim();
|
|
104
|
+
|
|
105
|
+
let entries = this.parseLines();
|
|
106
|
+
if (filterFor) entries = entries.filter((entry) => String(entry.for || "") === filterFor);
|
|
107
|
+
if (filterFrom) entries = entries.filter((entry) => String(entry.from || "") === filterFrom);
|
|
108
|
+
|
|
109
|
+
entries.sort((a, b) => {
|
|
110
|
+
const left = new Date(a.ts || 0).getTime();
|
|
111
|
+
const right = new Date(b.ts || 0).getTime();
|
|
112
|
+
return right - left;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const shown = entries.slice(0, num);
|
|
116
|
+
console.log(`=== Sync (${shown.length} shown, ${entries.length} matched) ===`);
|
|
117
|
+
for (const entry of shown) {
|
|
118
|
+
console.log(`${entry.ts || "-"} ${this.formatEntry(entry)}`);
|
|
119
|
+
}
|
|
120
|
+
if (shown.length === 0) {
|
|
121
|
+
console.log("No sync entries found.");
|
|
122
|
+
}
|
|
123
|
+
return shown;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = SyncManager;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventBus = require("../bus");
|
|
4
|
+
|
|
5
|
+
class AgentProcessManager {
|
|
6
|
+
constructor(projectRoot) {
|
|
7
|
+
this.projectRoot = projectRoot;
|
|
8
|
+
this.processes = new Map(); // subscriber_id -> child_process
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 注册子进程并监听退出事件
|
|
13
|
+
*/
|
|
14
|
+
register(subscriberId, childProcess) {
|
|
15
|
+
if (!subscriberId || !childProcess) return;
|
|
16
|
+
|
|
17
|
+
this.processes.set(subscriberId, childProcess);
|
|
18
|
+
|
|
19
|
+
childProcess.on("exit", (code, signal) => {
|
|
20
|
+
this.processes.delete(subscriberId);
|
|
21
|
+
|
|
22
|
+
// 自动清理 bus 状态
|
|
23
|
+
try {
|
|
24
|
+
const eventBus = new EventBus(this.projectRoot);
|
|
25
|
+
eventBus.loadBusData();
|
|
26
|
+
if (eventBus.busData.agents?.[subscriberId]) {
|
|
27
|
+
eventBus.busData.agents[subscriberId].status = "inactive";
|
|
28
|
+
eventBus.busData.agents[subscriberId].last_seen = new Date().toISOString();
|
|
29
|
+
eventBus.saveBusData();
|
|
30
|
+
console.log(`[daemon] Agent ${subscriberId} exited (code=${code}, signal=${signal}), marked inactive`);
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(`[daemon] Failed to cleanup ${subscriberId}:`, err.message);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
childProcess.on("error", (err) => {
|
|
38
|
+
console.error(`[daemon] Agent ${subscriberId} error:`, err.message);
|
|
39
|
+
this.processes.delete(subscriberId);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 获取运行中的进程
|
|
45
|
+
*/
|
|
46
|
+
get(subscriberId) {
|
|
47
|
+
return this.processes.get(subscriberId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取所有进程数量
|
|
52
|
+
*/
|
|
53
|
+
count() {
|
|
54
|
+
return this.processes.size;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 清理所有子进程
|
|
59
|
+
*/
|
|
60
|
+
cleanup() {
|
|
61
|
+
for (const [subscriberId, child] of this.processes.entries()) {
|
|
62
|
+
try {
|
|
63
|
+
child.kill("SIGTERM");
|
|
64
|
+
console.log(`[daemon] Killed agent ${subscriberId}`);
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.processes.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
module.exports = { AgentProcessManager };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createCronScheduler,
|
|
3
|
+
parseIntervalMs,
|
|
4
|
+
formatIntervalMs,
|
|
5
|
+
} = require("../chat/cronScheduler");
|
|
6
|
+
|
|
7
|
+
function splitTargets(value = "") {
|
|
8
|
+
return String(value || "")
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((item) => item.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeCronTargets(op = {}) {
|
|
15
|
+
const fromArray = Array.isArray(op.targets)
|
|
16
|
+
? op.targets.map((item) => String(item || "").trim()).filter(Boolean)
|
|
17
|
+
: [];
|
|
18
|
+
if (fromArray.length > 0) return Array.from(new Set(fromArray));
|
|
19
|
+
|
|
20
|
+
const merged = [
|
|
21
|
+
op.target,
|
|
22
|
+
op.agent,
|
|
23
|
+
op.to,
|
|
24
|
+
]
|
|
25
|
+
.map((item) => String(item || "").trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(",");
|
|
28
|
+
|
|
29
|
+
return Array.from(new Set(splitTargets(merged)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveCronOperation(op = {}) {
|
|
33
|
+
const raw = String(op.operation || op.op || op.command || "").trim().toLowerCase();
|
|
34
|
+
if (raw) return raw;
|
|
35
|
+
if (op.list === true) return "list";
|
|
36
|
+
if (op.stop === true) return "stop";
|
|
37
|
+
if (op.id || op.task_id || op.taskId) return "stop";
|
|
38
|
+
return "start";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveCronIntervalMs(op = {}) {
|
|
42
|
+
const numeric = Number(op.interval_ms ?? op.intervalMs);
|
|
43
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
44
|
+
return Math.floor(numeric);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const everyRaw = String(op.every || op.interval || op.ms || "").trim();
|
|
48
|
+
if (!everyRaw) return 0;
|
|
49
|
+
return parseIntervalMs(everyRaw);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveCronPrompt(op = {}) {
|
|
53
|
+
return String(op.prompt || op.message || op.msg || "").trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveCronTaskId(op = {}) {
|
|
57
|
+
return String(op.id || op.task_id || op.taskId || "").trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatCronTask(task = {}) {
|
|
61
|
+
return {
|
|
62
|
+
id: String(task.id || ""),
|
|
63
|
+
intervalMs: Number(task.intervalMs) || 0,
|
|
64
|
+
interval: formatIntervalMs(task.intervalMs || 0),
|
|
65
|
+
targets: Array.isArray(task.targets) ? task.targets.slice() : [],
|
|
66
|
+
prompt: String(task.prompt || ""),
|
|
67
|
+
createdAt: Number(task.createdAt) || 0,
|
|
68
|
+
lastRunAt: Number(task.lastRunAt) || 0,
|
|
69
|
+
tickCount: Number(task.tickCount) || 0,
|
|
70
|
+
summary: String(task.summary || ""),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createDaemonCronController(options = {}) {
|
|
75
|
+
const {
|
|
76
|
+
dispatch = async () => {},
|
|
77
|
+
log = () => {},
|
|
78
|
+
setIntervalFn,
|
|
79
|
+
clearIntervalFn,
|
|
80
|
+
nowFn,
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
const scheduler = createCronScheduler({
|
|
84
|
+
dispatch: ({ taskId, target, message }) => {
|
|
85
|
+
try {
|
|
86
|
+
Promise.resolve(dispatch({ taskId, target, message })).catch((err) => {
|
|
87
|
+
const detail = err && err.message ? err.message : String(err || "dispatch failed");
|
|
88
|
+
log(`cron dispatch failed task=${taskId} target=${target}: ${detail}`);
|
|
89
|
+
});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const detail = err && err.message ? err.message : String(err || "dispatch failed");
|
|
92
|
+
log(`cron dispatch failed task=${taskId} target=${target}: ${detail}`);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
setIntervalFn,
|
|
96
|
+
clearIntervalFn,
|
|
97
|
+
nowFn,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function listTasks() {
|
|
101
|
+
return scheduler.listTasks().map(formatCronTask);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleCronOp(op = {}) {
|
|
105
|
+
const operation = resolveCronOperation(op);
|
|
106
|
+
|
|
107
|
+
if (operation === "list" || operation === "ls") {
|
|
108
|
+
const tasks = listTasks();
|
|
109
|
+
return {
|
|
110
|
+
action: "cron",
|
|
111
|
+
operation: "list",
|
|
112
|
+
ok: true,
|
|
113
|
+
count: tasks.length,
|
|
114
|
+
tasks,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (operation === "stop" || operation === "rm" || operation === "remove") {
|
|
119
|
+
const id = resolveCronTaskId(op);
|
|
120
|
+
if (!id) {
|
|
121
|
+
return {
|
|
122
|
+
action: "cron",
|
|
123
|
+
operation: "stop",
|
|
124
|
+
ok: false,
|
|
125
|
+
error: "cron stop requires id or all",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (id === "all") {
|
|
130
|
+
const stopped = scheduler.stopAll();
|
|
131
|
+
return {
|
|
132
|
+
action: "cron",
|
|
133
|
+
operation: "stop",
|
|
134
|
+
ok: true,
|
|
135
|
+
id: "all",
|
|
136
|
+
stopped,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ok = scheduler.stopTask(id);
|
|
141
|
+
if (!ok) {
|
|
142
|
+
return {
|
|
143
|
+
action: "cron",
|
|
144
|
+
operation: "stop",
|
|
145
|
+
ok: false,
|
|
146
|
+
id,
|
|
147
|
+
error: `cron task not found: ${id}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
action: "cron",
|
|
153
|
+
operation: "stop",
|
|
154
|
+
ok: true,
|
|
155
|
+
id,
|
|
156
|
+
stopped: 1,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (operation !== "start" && operation !== "add" && operation !== "create") {
|
|
161
|
+
return {
|
|
162
|
+
action: "cron",
|
|
163
|
+
operation,
|
|
164
|
+
ok: false,
|
|
165
|
+
error: `unsupported cron operation: ${operation}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const intervalMs = resolveCronIntervalMs(op);
|
|
170
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 1000) {
|
|
171
|
+
return {
|
|
172
|
+
action: "cron",
|
|
173
|
+
operation: "start",
|
|
174
|
+
ok: false,
|
|
175
|
+
error: "invalid cron interval (min 1s)",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const targets = normalizeCronTargets(op);
|
|
180
|
+
if (targets.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
action: "cron",
|
|
183
|
+
operation: "start",
|
|
184
|
+
ok: false,
|
|
185
|
+
error: "cron start requires at least one target",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const prompt = resolveCronPrompt(op);
|
|
190
|
+
if (!prompt) {
|
|
191
|
+
return {
|
|
192
|
+
action: "cron",
|
|
193
|
+
operation: "start",
|
|
194
|
+
ok: false,
|
|
195
|
+
error: "cron start requires prompt",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const task = scheduler.addTask({
|
|
200
|
+
intervalMs,
|
|
201
|
+
targets,
|
|
202
|
+
prompt,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!task) {
|
|
206
|
+
return {
|
|
207
|
+
action: "cron",
|
|
208
|
+
operation: "start",
|
|
209
|
+
ok: false,
|
|
210
|
+
error: "failed to create cron task",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
action: "cron",
|
|
216
|
+
operation: "start",
|
|
217
|
+
ok: true,
|
|
218
|
+
task: formatCronTask(task),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stopAll() {
|
|
223
|
+
return scheduler.stopAll();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
handleCronOp,
|
|
228
|
+
listTasks,
|
|
229
|
+
stopAll,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
createDaemonCronController,
|
|
235
|
+
normalizeCronTargets,
|
|
236
|
+
resolveCronOperation,
|
|
237
|
+
resolveCronIntervalMs,
|
|
238
|
+
resolveCronPrompt,
|
|
239
|
+
resolveCronTaskId,
|
|
240
|
+
formatCronTask,
|
|
241
|
+
};
|