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.
Files changed (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. 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 = { loadConfig, saveConfig, normalizeLaunchMode, normalizeAgentProvider };
96
+ module.exports = {
97
+ loadConfig,
98
+ saveConfig,
99
+ normalizeLaunchMode,
100
+ normalizeAgentProvider,
101
+ normalizeAssistantEngine,
102
+ };
@@ -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\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`;
@@ -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
- console.log(`Context: ${openDecisions.length} open decision(s), ${decisions.length} total`);
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
+ };