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
@@ -0,0 +1,132 @@
1
+ const COMMAND_TREE = {
2
+ "/bus": {
3
+ desc: "Event bus operations",
4
+ children: {
5
+ activate: { desc: "Activate agent terminal" },
6
+ list: { desc: "List all agents" },
7
+ rename: { desc: "Rename agent nickname" },
8
+ send: { desc: "Send message to agent" },
9
+ status: { desc: "Bus status" },
10
+ },
11
+ },
12
+ "/ctx": {
13
+ desc: "Context management",
14
+ children: {
15
+ decisions: { desc: "List all decisions" },
16
+ doctor: { desc: "Check context integrity" },
17
+ status: { desc: "Show context status (default)" },
18
+ },
19
+ },
20
+ "/daemon": {
21
+ desc: "Daemon management",
22
+ children: {
23
+ restart: { desc: "Restart daemon" },
24
+ start: { desc: "Start daemon" },
25
+ status: { desc: "Daemon status" },
26
+ stop: { desc: "Stop daemon" },
27
+ },
28
+ },
29
+ "/doctor": { desc: "Health check diagnostics" },
30
+ "/corn": {
31
+ desc: "Cron scheduler operations",
32
+ children: {
33
+ start: { desc: "Create cron task" },
34
+ list: { desc: "List cron tasks" },
35
+ stop: { desc: "Stop cron task by id or all" },
36
+ },
37
+ },
38
+ "/init": { desc: "Initialize modules" },
39
+ "/launch": {
40
+ desc: "Launch new agent",
41
+ children: {
42
+ claude: { desc: "Launch Claude agent" },
43
+ codex: { desc: "Launch Codex agent" },
44
+ ucode: { desc: "Launch ucode core agent" },
45
+ },
46
+ },
47
+ "/resume": {
48
+ desc: "Resume agents (optional nickname) or list recoverable targets",
49
+ children: {
50
+ list: { desc: "List recoverable agents (optional target)" },
51
+ },
52
+ },
53
+ "/settings": {
54
+ desc: "Settings operations",
55
+ children: {
56
+ ucode: { desc: "Manage ucode model provider config" },
57
+ },
58
+ },
59
+ "/skills": {
60
+ desc: "Skills management",
61
+ children: {
62
+ install: { desc: "Install skills (use: all or name)" },
63
+ list: { desc: "List available skills" },
64
+ },
65
+ },
66
+ "/status": { desc: "Status display" },
67
+ };
68
+
69
+ const COMMAND_ORDER = ["/launch", "/bus", "/ctx"];
70
+ const COMMAND_ORDER_MAP = new Map(COMMAND_ORDER.map((cmd, idx) => [cmd, idx]));
71
+
72
+ function sortCommands(a, b) {
73
+ const ai = COMMAND_ORDER_MAP.has(a) ? COMMAND_ORDER_MAP.get(a) : Number.POSITIVE_INFINITY;
74
+ const bi = COMMAND_ORDER_MAP.has(b) ? COMMAND_ORDER_MAP.get(b) : Number.POSITIVE_INFINITY;
75
+ if (ai !== bi) return ai - bi;
76
+ return a.localeCompare(b, "en", { sensitivity: "base" });
77
+ }
78
+
79
+ function buildCommandRegistry(tree) {
80
+ return Object.keys(tree)
81
+ .sort(sortCommands)
82
+ .map((cmd) => {
83
+ const node = tree[cmd] || {};
84
+ const entry = { cmd, desc: node.desc || "" };
85
+ if (node.children) {
86
+ entry.subcommands = Object.keys(node.children)
87
+ .sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }))
88
+ .map((sub) => ({
89
+ cmd: sub,
90
+ desc: (node.children[sub] && node.children[sub].desc) || "",
91
+ }));
92
+ }
93
+ return entry;
94
+ });
95
+ }
96
+
97
+ const COMMAND_REGISTRY = buildCommandRegistry(COMMAND_TREE);
98
+
99
+ function parseCommand(text) {
100
+ if (!text.startsWith("/")) return null;
101
+
102
+ // Split by whitespace, respecting quotes
103
+ const parts = text.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
104
+ if (parts.length === 0) return null;
105
+
106
+ const command = parts[0].slice(1); // Remove leading /
107
+ const args = parts.slice(1).map((arg) => arg.replace(/^"|"$/g, "")); // Remove quotes
108
+
109
+ return { command, args };
110
+ }
111
+
112
+ function parseAtTarget(text) {
113
+ if (!text.startsWith("@")) return null;
114
+ const trimmed = text.slice(1).trim();
115
+ if (!trimmed) return null;
116
+ const spaceIdx = trimmed.indexOf(" ");
117
+ if (spaceIdx === -1) {
118
+ return { target: trimmed, message: "" };
119
+ }
120
+ const target = trimmed.slice(0, spaceIdx).trim();
121
+ const message = trimmed.slice(spaceIdx + 1).trim();
122
+ return { target, message };
123
+ }
124
+
125
+ module.exports = {
126
+ COMMAND_TREE,
127
+ COMMAND_REGISTRY,
128
+ sortCommands,
129
+ buildCommandRegistry,
130
+ parseCommand,
131
+ parseAtTarget,
132
+ };
@@ -0,0 +1,414 @@
1
+ const FALLBACK_LAUNCH_SUBCOMMANDS = [
2
+ { cmd: "claude", desc: "Launch Claude agent" },
3
+ { cmd: "codex", desc: "Launch Codex agent" },
4
+ { cmd: "ucode", desc: "Launch ucode core agent" },
5
+ ];
6
+
7
+ function createCompletionController(options = {}) {
8
+ const {
9
+ input,
10
+ screen,
11
+ completionPanel,
12
+ promptBox,
13
+ commandRegistry = [],
14
+ getMentionCandidates = () => [],
15
+ normalizeCommandPrefix = () => {},
16
+ truncateText = (text) => String(text || ""),
17
+ getCurrentInputHeight = () => 4,
18
+ getCursorPos = () => 0,
19
+ setCursorPos = () => {},
20
+ resetPreferredCol = () => {},
21
+ updateDraftFromInput = () => {},
22
+ renderScreen = () => {},
23
+ setImmediateFn = setImmediate,
24
+ clearImmediateFn = clearImmediate,
25
+ } = options;
26
+
27
+ if (!input || !screen || !completionPanel || !promptBox) {
28
+ throw new Error("createCompletionController requires input/screen/completionPanel/promptBox");
29
+ }
30
+
31
+ const state = {
32
+ active: false,
33
+ commands: [],
34
+ index: 0,
35
+ scrollOffset: 0,
36
+ visibleCount: 0,
37
+ enterSuppressed: false,
38
+ enterReset: null,
39
+ };
40
+
41
+ function setPanelLayout() {
42
+ const availableHeight = Math.max(1, screen.height - getCurrentInputHeight() - 1);
43
+ const maxVisible = Math.max(1, availableHeight - 2);
44
+ state.visibleCount = Math.min(7, state.commands.length, maxVisible);
45
+ completionPanel.height = Math.min(availableHeight, state.visibleCount + 2);
46
+ completionPanel.bottom = getCurrentInputHeight() - 1;
47
+ }
48
+
49
+ function render() {
50
+ if (!state.active || state.commands.length === 0) return;
51
+
52
+ const panelVisible = Math.max(1, (completionPanel.height || 1) - 2);
53
+ const maxVisible = state.visibleCount
54
+ ? Math.max(1, Math.min(state.visibleCount, panelVisible))
55
+ : panelVisible;
56
+
57
+ if (state.index < state.scrollOffset) {
58
+ state.scrollOffset = state.index;
59
+ } else if (state.index >= state.scrollOffset + maxVisible) {
60
+ state.scrollOffset = state.index - maxVisible + 1;
61
+ }
62
+
63
+ const visibleStart = state.scrollOffset;
64
+ const visibleEnd = Math.min(state.scrollOffset + maxVisible, state.commands.length);
65
+ const visibleCommands = state.commands.slice(visibleStart, visibleEnd);
66
+
67
+ const panelWidth = typeof completionPanel.width === "number"
68
+ ? completionPanel.width
69
+ : screen.width;
70
+
71
+ const lines = visibleCommands.map((item, i) => {
72
+ const actualIndex = visibleStart + i;
73
+ const cmdText = item.cmd;
74
+ const descText = item.desc || "";
75
+ const cmdPart = actualIndex === state.index
76
+ ? `{inverse}${cmdText}{/inverse}`
77
+ : `{cyan-fg}${cmdText}{/cyan-fg}`;
78
+ const indent = " ".repeat(promptBox.width || 2);
79
+ const maxDescWidth = Math.max(0, panelWidth - indent.length - cmdText.length - 2);
80
+ const trimmedDesc = truncateText(descText, maxDescWidth);
81
+ const descPart = trimmedDesc ? `{gray-fg}${trimmedDesc}{/gray-fg}` : "";
82
+ return descPart ? `${indent}${cmdPart} ${descPart}` : `${indent}${cmdPart}`;
83
+ });
84
+
85
+ completionPanel.setContent(lines.join("\n"));
86
+ renderScreen();
87
+ }
88
+
89
+ function hide() {
90
+ state.active = false;
91
+ state.commands = [];
92
+ state.index = 0;
93
+ state.scrollOffset = 0;
94
+ state.visibleCount = 0;
95
+ completionPanel.hidden = true;
96
+ renderScreen();
97
+ }
98
+
99
+ function buildCommands(filterText) {
100
+ const mentionMatch = String(filterText || "").match(/^@([^\s]*)$/);
101
+ if (mentionMatch) {
102
+ const mentionFilter = String(mentionMatch[1] || "").trim().toLowerCase();
103
+ const rawCandidates = Array.isArray(getMentionCandidates()) ? getMentionCandidates() : [];
104
+ const seen = new Set();
105
+ const items = [];
106
+ for (const item of rawCandidates) {
107
+ const id = String(item && item.id ? item.id : "").trim();
108
+ const label = String(item && item.label ? item.label : id).trim();
109
+ if (!id && !label) continue;
110
+ const rawToken = label || id;
111
+ const normalizedToken = rawToken.replace(/^@+/, "");
112
+ if (!normalizedToken || /\s/.test(normalizedToken)) continue;
113
+ const tokenLower = normalizedToken.toLowerCase();
114
+ const idLower = id.toLowerCase();
115
+ if (
116
+ mentionFilter
117
+ && !tokenLower.startsWith(mentionFilter)
118
+ && !idLower.startsWith(mentionFilter)
119
+ ) {
120
+ continue;
121
+ }
122
+ if (seen.has(normalizedToken)) continue;
123
+ seen.add(normalizedToken);
124
+ const desc = id && id !== normalizedToken ? id : "";
125
+ items.push({
126
+ cmd: `@${normalizedToken}`,
127
+ desc,
128
+ isMention: true,
129
+ mentionTarget: normalizedToken,
130
+ });
131
+ }
132
+ return items.sort((a, b) => a.cmd.localeCompare(b.cmd));
133
+ }
134
+
135
+ const endsWithSpace = /\s$/.test(filterText);
136
+ const trimmed = filterText.trim();
137
+ if (!trimmed) {
138
+ return [];
139
+ }
140
+
141
+ const parts = trimmed.split(/\s+/);
142
+ const mainCmd = parts[0];
143
+ const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
144
+ const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
145
+
146
+ if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
147
+ const subFilter = parts[1] || "";
148
+ const mainCmdObj = commandRegistry.find((item) =>
149
+ item.cmd.toLowerCase() === mainCmd.toLowerCase()
150
+ );
151
+ if ((mainCmdObj && mainCmdObj.subcommands) || isLaunch) {
152
+ const baseSubs = mainCmdObj && mainCmdObj.subcommands ? mainCmdObj.subcommands : [];
153
+ let subs = baseSubs;
154
+ if (isLaunch) {
155
+ const merged = new Map();
156
+ for (const sub of [...baseSubs, ...FALLBACK_LAUNCH_SUBCOMMANDS]) {
157
+ if (!sub || !sub.cmd) continue;
158
+ merged.set(sub.cmd, sub);
159
+ }
160
+ subs = Array.from(merged.values());
161
+ }
162
+ if (isLaunch) {
163
+ return subs
164
+ .map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
165
+ .sort((a, b) => a.cmd.localeCompare(b.cmd));
166
+ }
167
+ return subs
168
+ .filter((sub) => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
169
+ .map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
170
+ .sort((a, b) => a.cmd.localeCompare(b.cmd));
171
+ }
172
+ return [];
173
+ }
174
+
175
+ const filterLower = trimmed.toLowerCase();
176
+ return commandRegistry.filter((item) => item.cmd.toLowerCase().startsWith(filterLower));
177
+ }
178
+
179
+ function show(filterText) {
180
+ normalizeCommandPrefix();
181
+
182
+ let nextFilter = filterText;
183
+ if (nextFilter !== input.value) {
184
+ nextFilter = input.value;
185
+ }
186
+
187
+ if (nextFilter.startsWith("//")) {
188
+ nextFilter = nextFilter.replace(/^\/+/, "/");
189
+ input.value = nextFilter;
190
+ setCursorPos(Math.min(getCursorPos(), input.value.length));
191
+ }
192
+
193
+ if (!nextFilter) {
194
+ hide();
195
+ return;
196
+ }
197
+
198
+ const commands = buildCommands(nextFilter);
199
+ if (commands.length === 0) {
200
+ hide();
201
+ return;
202
+ }
203
+
204
+ state.commands = commands;
205
+ state.active = true;
206
+ state.index = 0;
207
+ state.scrollOffset = 0;
208
+ setPanelLayout();
209
+ completionPanel.hidden = false;
210
+ render();
211
+ }
212
+
213
+ function pageSize() {
214
+ const panelVisible = Math.max(1, (completionPanel.height || 2) - 2);
215
+ return state.visibleCount
216
+ ? Math.max(1, Math.min(state.visibleCount, panelVisible))
217
+ : panelVisible;
218
+ }
219
+
220
+ function up() {
221
+ if (state.commands.length === 0) return;
222
+ state.index = state.index <= 0 ? state.commands.length - 1 : state.index - 1;
223
+ render();
224
+ }
225
+
226
+ function down() {
227
+ if (state.commands.length === 0) return;
228
+ state.index = state.index >= state.commands.length - 1 ? 0 : state.index + 1;
229
+ render();
230
+ }
231
+
232
+ function pageUp() {
233
+ if (state.commands.length === 0) return;
234
+ state.index = Math.max(0, state.index - pageSize());
235
+ render();
236
+ }
237
+
238
+ function pageDown() {
239
+ if (state.commands.length === 0) return;
240
+ state.index = Math.min(state.commands.length - 1, state.index + pageSize());
241
+ render();
242
+ }
243
+
244
+ function preview(selected) {
245
+ const current = input.value || "";
246
+ const trimmed = current.trim();
247
+ const endsWithSpace = /\s$/.test(current);
248
+
249
+ if (selected.isMention) {
250
+ const mentionTarget = String(selected.mentionTarget || selected.cmd || "").replace(/^@+/, "");
251
+ const completedCore = `@${mentionTarget}`;
252
+ const isComplete = (trimmed === completedCore && endsWithSpace) || trimmed.startsWith(`${completedCore} `);
253
+ return { text: `${completedCore} `, isComplete };
254
+ }
255
+
256
+ if (selected.isSubcommand) {
257
+ const parts = trimmed.split(/\s+/);
258
+ const base = parts[0] || "";
259
+ const completedCore = base ? `${base} ${selected.cmd}` : selected.cmd;
260
+ const isComplete = trimmed === completedCore || trimmed.startsWith(`${completedCore} `);
261
+ return { text: `${completedCore} `, isComplete };
262
+ }
263
+
264
+ const completedCore = selected.cmd;
265
+ const hasChildren = selected.subcommands && selected.subcommands.length > 0;
266
+ const isComplete =
267
+ (trimmed === completedCore && (!hasChildren || endsWithSpace)) ||
268
+ trimmed.startsWith(`${completedCore} `);
269
+ return { text: `${completedCore} `, isComplete };
270
+ }
271
+
272
+ function applyPreview(nextPreview) {
273
+ input.value = nextPreview.text;
274
+ setCursorPos(input.value.length);
275
+ resetPreferredCol();
276
+ if (typeof input._updateCursor === "function") {
277
+ input._updateCursor();
278
+ }
279
+ updateDraftFromInput();
280
+ renderScreen();
281
+ }
282
+
283
+ function confirm() {
284
+ if (!state.active || state.commands.length === 0) return;
285
+
286
+ const selected = state.commands[state.index];
287
+ if (selected.isMention) {
288
+ const mentionTarget = String(selected.mentionTarget || selected.cmd || "").replace(/^@+/, "");
289
+ input.value = `@${mentionTarget} `;
290
+ } else if (selected.isSubcommand) {
291
+ const parts = input.value.split(/\s+/);
292
+ parts[parts.length - 1] = selected.cmd;
293
+ input.value = `${parts.join(" ")} `;
294
+ } else {
295
+ input.value = `${selected.cmd} `;
296
+ }
297
+
298
+ setCursorPos(input.value.length);
299
+ resetPreferredCol();
300
+ if (typeof input._updateCursor === "function") {
301
+ input._updateCursor();
302
+ }
303
+ updateDraftFromInput();
304
+
305
+ if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
306
+ show(input.value);
307
+ } else {
308
+ hide();
309
+ }
310
+
311
+ renderScreen();
312
+ }
313
+
314
+ function handleKey(ch, key = {}) {
315
+ if (!state.active) return false;
316
+
317
+ if (key.name === "up") {
318
+ up();
319
+ return true;
320
+ }
321
+ if (key.name === "down") {
322
+ down();
323
+ return true;
324
+ }
325
+ if (key.name === "tab") {
326
+ confirm();
327
+ return true;
328
+ }
329
+ if (key.name === "pageup") {
330
+ pageUp();
331
+ return true;
332
+ }
333
+ if (key.name === "pagedown") {
334
+ pageDown();
335
+ return true;
336
+ }
337
+
338
+ if (key.name === "enter" || key.name === "return") {
339
+ if (state.enterSuppressed) {
340
+ return true;
341
+ }
342
+ const selected = state.commands[state.index];
343
+ if (selected) {
344
+ const nextPreview = preview(selected);
345
+ if (!nextPreview.isComplete) {
346
+ applyPreview(nextPreview);
347
+ if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
348
+ show(input.value);
349
+ } else {
350
+ hide();
351
+ }
352
+ state.enterSuppressed = true;
353
+ if (state.enterReset) clearImmediateFn(state.enterReset);
354
+ state.enterReset = setImmediateFn(() => {
355
+ state.enterSuppressed = false;
356
+ });
357
+ return true;
358
+ }
359
+ }
360
+ hide();
361
+ state.enterSuppressed = true;
362
+ if (state.enterReset) clearImmediateFn(state.enterReset);
363
+ state.enterReset = setImmediateFn(() => {
364
+ state.enterSuppressed = false;
365
+ });
366
+ return false;
367
+ }
368
+
369
+ if (key.name === "escape") {
370
+ hide();
371
+ return true;
372
+ }
373
+
374
+ if (ch === " ") {
375
+ const currentInput = (input.value || "").trim();
376
+ if (currentInput.startsWith("/") && !currentInput.includes(" ")) {
377
+ return false;
378
+ }
379
+ if (currentInput.startsWith("@") && !currentInput.includes(" ")) {
380
+ return false;
381
+ }
382
+ hide();
383
+ return false;
384
+ }
385
+
386
+ return false;
387
+ }
388
+
389
+ function reflow() {
390
+ if (!state.active) return;
391
+ setPanelLayout();
392
+ render();
393
+ }
394
+
395
+ function jumpToLast() {
396
+ if (state.commands.length === 0) return;
397
+ state.index = state.commands.length - 1;
398
+ render();
399
+ }
400
+
401
+ return {
402
+ show,
403
+ hide,
404
+ handleKey,
405
+ reflow,
406
+ isActive: () => state.active,
407
+ getCommandCount: () => state.commands.length,
408
+ jumpToLast,
409
+ };
410
+ }
411
+
412
+ module.exports = {
413
+ createCompletionController,
414
+ };
@@ -0,0 +1,160 @@
1
+ function parseIntervalMs(value = "") {
2
+ const text = String(value || "").trim().toLowerCase();
3
+ if (!text) return 0;
4
+ const match = text.match(/^(\d+)(ms|s|m|h)?$/);
5
+ if (!match) return 0;
6
+ const amount = Number.parseInt(match[1], 10);
7
+ if (!Number.isFinite(amount) || amount <= 0) return 0;
8
+ const unit = match[2] || "s";
9
+ if (unit === "ms") return amount;
10
+ if (unit === "s") return amount * 1000;
11
+ if (unit === "m") return amount * 60 * 1000;
12
+ if (unit === "h") return amount * 60 * 60 * 1000;
13
+ return 0;
14
+ }
15
+
16
+ function formatIntervalMs(ms = 0) {
17
+ const value = Number(ms) || 0;
18
+ if (value <= 0) return "0s";
19
+ if (value % (60 * 60 * 1000) === 0) return `${value / (60 * 60 * 1000)}h`;
20
+ if (value % (60 * 1000) === 0) return `${value / (60 * 1000)}m`;
21
+ if (value % 1000 === 0) return `${value / 1000}s`;
22
+ return `${value}ms`;
23
+ }
24
+
25
+ function sanitizeSummaryText(value = "") {
26
+ return String(value || "")
27
+ .replace(/[{}]/g, "")
28
+ .replace(/\s+/g, " ")
29
+ .trim();
30
+ }
31
+
32
+ function summarizeTask(task = {}) {
33
+ const id = String(task.id || "");
34
+ const interval = formatIntervalMs(task.intervalMs || 0);
35
+ const targets = Array.isArray(task.targets) ? task.targets.join("+") : "";
36
+ const promptRaw = sanitizeSummaryText(task.prompt || "");
37
+ const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
38
+ return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
39
+ }
40
+
41
+ function createCronScheduler(options = {}) {
42
+ const {
43
+ dispatch = () => {},
44
+ onChange = () => {},
45
+ setIntervalFn = setInterval,
46
+ clearIntervalFn = clearInterval,
47
+ nowFn = () => Date.now(),
48
+ } = options;
49
+
50
+ let seq = 0;
51
+ const tasks = [];
52
+
53
+ function notifyChange() {
54
+ try {
55
+ onChange();
56
+ } catch {
57
+ // ignore observer errors
58
+ }
59
+ }
60
+
61
+ function addTask({ intervalMs = 0, targets = [], prompt = "" } = {}) {
62
+ const safeInterval = Number.parseInt(intervalMs, 10);
63
+ const safeTargets = Array.isArray(targets)
64
+ ? targets.map((item) => String(item || "").trim()).filter(Boolean)
65
+ : [];
66
+ const safePrompt = String(prompt || "").trim();
67
+ if (!Number.isFinite(safeInterval) || safeInterval <= 0) return null;
68
+ if (safeTargets.length === 0) return null;
69
+ if (!safePrompt) return null;
70
+
71
+ const id = `c${++seq}`;
72
+ const task = {
73
+ id,
74
+ intervalMs: safeInterval,
75
+ targets: Array.from(new Set(safeTargets)),
76
+ prompt: safePrompt,
77
+ createdAt: nowFn(),
78
+ lastRunAt: 0,
79
+ tickCount: 0,
80
+ timer: null,
81
+ };
82
+
83
+ task.timer = setIntervalFn(() => {
84
+ task.lastRunAt = nowFn();
85
+ task.tickCount += 1;
86
+ for (const target of task.targets) {
87
+ try {
88
+ dispatch({
89
+ taskId: task.id,
90
+ target,
91
+ message: task.prompt,
92
+ });
93
+ } catch {
94
+ // ignore single-dispatch errors
95
+ }
96
+ }
97
+ }, task.intervalMs);
98
+
99
+ tasks.push(task);
100
+ notifyChange();
101
+ return {
102
+ ...task,
103
+ summary: summarizeTask(task),
104
+ };
105
+ }
106
+
107
+ function listTasks() {
108
+ return tasks.map((task) => ({
109
+ id: task.id,
110
+ intervalMs: task.intervalMs,
111
+ targets: task.targets.slice(),
112
+ prompt: task.prompt,
113
+ createdAt: task.createdAt,
114
+ lastRunAt: task.lastRunAt,
115
+ tickCount: task.tickCount,
116
+ summary: summarizeTask(task),
117
+ }));
118
+ }
119
+
120
+ function stopTask(taskId = "") {
121
+ const id = String(taskId || "").trim();
122
+ if (!id) return false;
123
+ const idx = tasks.findIndex((task) => task.id === id);
124
+ if (idx < 0) return false;
125
+ const task = tasks[idx];
126
+ if (task && task.timer) {
127
+ clearIntervalFn(task.timer);
128
+ }
129
+ tasks.splice(idx, 1);
130
+ notifyChange();
131
+ return true;
132
+ }
133
+
134
+ function stopAll() {
135
+ if (tasks.length === 0) return 0;
136
+ const count = tasks.length;
137
+ while (tasks.length > 0) {
138
+ const task = tasks.pop();
139
+ if (task && task.timer) {
140
+ clearIntervalFn(task.timer);
141
+ }
142
+ }
143
+ notifyChange();
144
+ return count;
145
+ }
146
+
147
+ return {
148
+ addTask,
149
+ listTasks,
150
+ stopTask,
151
+ stopAll,
152
+ };
153
+ }
154
+
155
+ module.exports = {
156
+ parseIntervalMs,
157
+ formatIntervalMs,
158
+ summarizeTask,
159
+ createCronScheduler,
160
+ };