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
|
@@ -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
|
+
};
|