@hybridaione/hybridclaw 0.1.5
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/.env.example +14 -0
- package/.github/workflows/pages.yml +41 -0
- package/AGENTS.md +24 -0
- package/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/SECURITY.md +67 -0
- package/config.example.json +62 -0
- package/container/Dockerfile +23 -0
- package/container/package-lock.json +246 -0
- package/container/package.json +17 -0
- package/container/src/hybridai-client.ts +38 -0
- package/container/src/index.ts +198 -0
- package/container/src/ipc.ts +37 -0
- package/container/src/tools.ts +1008 -0
- package/container/src/types.ts +74 -0
- package/container/src/web-fetch.ts +389 -0
- package/container/tsconfig.json +16 -0
- package/dist/agent.d.ts +3 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +21 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +32 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +107 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +11 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +246 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/conversation.d.ts +18 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +29 -0
- package/dist/conversation.js.map +1 -0
- package/dist/db.d.ts +29 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +205 -0
- package/dist/db.js.map +1 -0
- package/dist/discord.d.ts +17 -0
- package/dist/discord.d.ts.map +1 -0
- package/dist/discord.js +115 -0
- package/dist/discord.js.map +1 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +36 -0
- package/dist/env.js.map +1 -0
- package/dist/gateway-client.d.ts +8 -0
- package/dist/gateway-client.d.ts.map +1 -0
- package/dist/gateway-client.js +57 -0
- package/dist/gateway-client.js.map +1 -0
- package/dist/gateway-service.d.ts +23 -0
- package/dist/gateway-service.d.ts.map +1 -0
- package/dist/gateway-service.js +360 -0
- package/dist/gateway-service.js.map +1 -0
- package/dist/gateway-types.d.ts +40 -0
- package/dist/gateway-types.d.ts.map +1 -0
- package/dist/gateway-types.js +6 -0
- package/dist/gateway-types.js.map +1 -0
- package/dist/gateway.d.ts +2 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +138 -0
- package/dist/gateway.js.map +1 -0
- package/dist/hatch.d.ts +7 -0
- package/dist/hatch.d.ts.map +1 -0
- package/dist/hatch.js +99 -0
- package/dist/hatch.js.map +1 -0
- package/dist/health.d.ts +2 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +169 -0
- package/dist/health.js.map +1 -0
- package/dist/heartbeat.d.ts +3 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +103 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/hybridai-bots.d.ts +5 -0
- package/dist/hybridai-bots.d.ts.map +1 -0
- package/dist/hybridai-bots.js +34 -0
- package/dist/hybridai-bots.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc.d.ts +33 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +142 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +21 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +28 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +187 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/onboarding.d.ts +7 -0
- package/dist/onboarding.d.ts.map +1 -0
- package/dist/onboarding.js +445 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/prompt-hooks.d.ts +17 -0
- package/dist/prompt-hooks.d.ts.map +1 -0
- package/dist/prompt-hooks.js +83 -0
- package/dist/prompt-hooks.js.map +1 -0
- package/dist/runtime-config.d.ts +78 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +471 -0
- package/dist/runtime-config.js.map +1 -0
- package/dist/scheduled-task-runner.d.ts +11 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -0
- package/dist/scheduled-task-runner.js +16 -0
- package/dist/scheduled-task-runner.js.map +1 -0
- package/dist/scheduler.d.ts +11 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +165 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/session-maintenance.d.ts +9 -0
- package/dist/session-maintenance.d.ts.map +1 -0
- package/dist/session-maintenance.js +168 -0
- package/dist/session-maintenance.js.map +1 -0
- package/dist/session-transcripts.d.ts +11 -0
- package/dist/session-transcripts.d.ts.map +1 -0
- package/dist/session-transcripts.js +32 -0
- package/dist/session-transcripts.js.map +1 -0
- package/dist/side-effects.d.ts +3 -0
- package/dist/side-effects.d.ts.map +1 -0
- package/dist/side-effects.js +30 -0
- package/dist/side-effects.js.map +1 -0
- package/dist/skills.d.ts +32 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +376 -0
- package/dist/skills.js.map +1 -0
- package/dist/tui.d.ts +2 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +305 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +25 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +154 -0
- package/dist/workspace.js.map +1 -0
- package/docs/chat.html +929 -0
- package/docs/hai_logo_free.png +0 -0
- package/docs/hero.png +0 -0
- package/docs/index.html +1213 -0
- package/package.json +34 -0
- package/skills/current-time/SKILL.md +26 -0
- package/skills/iss-position/SKILL.md +46 -0
- package/skills/iss-position/agents/openai.yaml +3 -0
- package/skills/iss-position/scripts/get_iss_position.py +107 -0
- package/skills/repo-orientation/SKILL.md +74 -0
- package/src/agent.ts +35 -0
- package/src/cli.ts +35 -0
- package/src/config.ts +137 -0
- package/src/container-runner.ts +305 -0
- package/src/conversation.ts +49 -0
- package/src/db.ts +290 -0
- package/src/discord.ts +156 -0
- package/src/env.ts +36 -0
- package/src/gateway-client.ts +73 -0
- package/src/gateway-service.ts +456 -0
- package/src/gateway-types.ts +47 -0
- package/src/gateway.ts +199 -0
- package/src/health.ts +189 -0
- package/src/heartbeat.ts +121 -0
- package/src/hybridai-bots.ts +48 -0
- package/src/ipc.ts +163 -0
- package/src/logger.ts +26 -0
- package/src/mount-security.ts +216 -0
- package/src/onboarding.ts +569 -0
- package/src/prompt-hooks.ts +113 -0
- package/src/runtime-config.ts +588 -0
- package/src/scheduled-task-runner.ts +26 -0
- package/src/scheduler.ts +196 -0
- package/src/session-maintenance.ts +263 -0
- package/src/session-transcripts.ts +45 -0
- package/src/side-effects.ts +41 -0
- package/src/skills.ts +429 -0
- package/src/tui.ts +327 -0
- package/src/types.ts +135 -0
- package/src/workspace.ts +171 -0
- package/templates/AGENTS.md +104 -0
- package/templates/BOOT.md +3 -0
- package/templates/BOOTSTRAP.md +54 -0
- package/templates/HEARTBEAT.md +5 -0
- package/templates/IDENTITY.md +18 -0
- package/templates/MEMORY.md +19 -0
- package/templates/SOUL.md +36 -0
- package/templates/TOOLS.md +22 -0
- package/templates/USER.md +17 -0
- package/tsconfig.json +18 -0
package/src/tui.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HybridClaw TUI — thin client for the gateway API.
|
|
3
|
+
* Usage: npm run tui
|
|
4
|
+
*/
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
|
|
7
|
+
import { APP_VERSION, GATEWAY_BASE_URL, HYBRIDAI_BASE_URL, HYBRIDAI_CHATBOT_ID, HYBRIDAI_MODEL } from './config.js';
|
|
8
|
+
import {
|
|
9
|
+
gatewayChat,
|
|
10
|
+
gatewayCommand,
|
|
11
|
+
gatewayStatus,
|
|
12
|
+
renderGatewayCommand,
|
|
13
|
+
type GatewayCommandResult,
|
|
14
|
+
} from './gateway-client.js';
|
|
15
|
+
import { logger } from './logger.js';
|
|
16
|
+
|
|
17
|
+
const RESET = '\x1b[0m';
|
|
18
|
+
const BOLD = '\x1b[1m';
|
|
19
|
+
const DIM = '\x1b[2m';
|
|
20
|
+
const TEAL = '\x1b[38;2;92;224;216m';
|
|
21
|
+
const NAVY = '\x1b[38;2;30;58;95m';
|
|
22
|
+
const GOLD = '\x1b[38;2;255;215;0m';
|
|
23
|
+
const GREEN = '\x1b[38;2;16;185;129m';
|
|
24
|
+
const RED = '\x1b[38;2;239;68;68m';
|
|
25
|
+
|
|
26
|
+
const SESSION_ID = 'tui:local';
|
|
27
|
+
const CHANNEL_ID = 'tui';
|
|
28
|
+
const TUI_MULTILINE_PASTE_DEBOUNCE_MS = Math.max(
|
|
29
|
+
20,
|
|
30
|
+
parseInt(process.env.TUI_MULTILINE_PASTE_DEBOUNCE_MS || '90', 10) || 90,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let activeRunAbortController: AbortController | null = null;
|
|
34
|
+
|
|
35
|
+
function printBanner(): void {
|
|
36
|
+
const T = TEAL;
|
|
37
|
+
const N = NAVY;
|
|
38
|
+
const logo = [
|
|
39
|
+
`${T} #### ####${RESET}`,
|
|
40
|
+
`${T} ######### #########${RESET}`,
|
|
41
|
+
`${T} #### # #${N} #####${RESET}`,
|
|
42
|
+
`${T} ### # #${N} ####${RESET}`,
|
|
43
|
+
`${T} ## # #${N} ###${RESET}`,
|
|
44
|
+
`${T} ## ## # #${N} # ###${RESET}`,
|
|
45
|
+
`${T} ####### ## ##### # #${N} #### ##${RESET}`,
|
|
46
|
+
`${T}##### ##### ## ### ### #${N}# # ## ${N}######${RESET}`,
|
|
47
|
+
`${T}## ##### ## # ${N}## ## ## ##${RESET}`,
|
|
48
|
+
`${T} ### ### ## # ${N}# ## ####${RESET}`,
|
|
49
|
+
`${T} # ## ### ${N}## #### ##${RESET}`,
|
|
50
|
+
`${T} ## # ## ${N}## #### ## ##${RESET}`,
|
|
51
|
+
`${T} #### ## # ${N}# # #### ## ## ${N}###${RESET}`,
|
|
52
|
+
`${T} #### #### ## ${N}# # ### ### ## ${N}####${RESET}`,
|
|
53
|
+
`${T} #### #### # ${N}# # ########### ##${RESET}`,
|
|
54
|
+
`${T}###### ### ## ##${N} ##### #### ### ${N}#####${RESET}`,
|
|
55
|
+
`${T} ## ## # ${N} ### ### ###${RESET}`,
|
|
56
|
+
`${T} ## # ###${N}###### ## # ##${RESET}`,
|
|
57
|
+
`${T} ## # # ${N} ####### ## ## ##${RESET}`,
|
|
58
|
+
`${T} ## # # ## ${N} ###### # ## ##${RESET}`,
|
|
59
|
+
`${T} ## # #####${N}###### #### # ##${RESET}`,
|
|
60
|
+
`${T} # ${N} ### ## # # # #${RESET}`,
|
|
61
|
+
`${T} # ### # # # ###${N} ### ## ### ######${RESET}`,
|
|
62
|
+
`${T} # ### ## #${N}######### ## ## ######${RESET}`,
|
|
63
|
+
`${T} # # ${N} ### ###${RESET}`,
|
|
64
|
+
`${T} #### ### ##${N}##### ###### ### ${N}####${RESET}`,
|
|
65
|
+
`${T} ### ${N} # ## #####${RESET}`,
|
|
66
|
+
`${T} ###${N}### # ##########${RESET}`,
|
|
67
|
+
`${T} ${N} ## ###${RESET}`,
|
|
68
|
+
];
|
|
69
|
+
console.log();
|
|
70
|
+
for (const line of logo) console.log(line);
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(` \u{1F99E} ${BOLD}${TEAL}H y b r i d ${GOLD}C l a w${RESET} ${DIM}v${APP_VERSION}${RESET}`);
|
|
73
|
+
console.log(`${DIM} Powered by HybridAI${RESET}`);
|
|
74
|
+
console.log();
|
|
75
|
+
console.log(` ${DIM}Model: ${TEAL}${HYBRIDAI_MODEL}${RESET}${DIM} | Bot: ${GOLD}${HYBRIDAI_CHATBOT_ID || 'unset'}${RESET}`);
|
|
76
|
+
console.log(` ${DIM}Gateway: ${TEAL}${GATEWAY_BASE_URL}${RESET}`);
|
|
77
|
+
console.log(` ${DIM}HybridAI: ${TEAL}${HYBRIDAI_BASE_URL}${RESET}`);
|
|
78
|
+
console.log();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function printHelp(): void {
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(` ${BOLD}${GOLD}Commands${RESET}`);
|
|
84
|
+
console.log(` ${TEAL}/help${RESET} Show this help`);
|
|
85
|
+
console.log(` ${TEAL}/bots${RESET} List available bots`);
|
|
86
|
+
console.log(` ${TEAL}/bot <id|name>${RESET} Switch bot for this session`);
|
|
87
|
+
console.log(` ${TEAL}/rag [on|off]${RESET} Toggle or set RAG`);
|
|
88
|
+
console.log(` ${TEAL}/info${RESET} Show current settings`);
|
|
89
|
+
console.log(` ${TEAL}/clear${RESET} Clear session history`);
|
|
90
|
+
console.log(` ${TEAL}/stop${RESET} Interrupt current request`);
|
|
91
|
+
console.log(` ${TEAL}/exit${RESET} Quit`);
|
|
92
|
+
console.log(` ${TEAL}ESC${RESET} Interrupt current request`);
|
|
93
|
+
console.log();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printResponse(text: string): void {
|
|
97
|
+
console.log();
|
|
98
|
+
for (const line of text.split('\n')) {
|
|
99
|
+
console.log(` ${line}`);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printError(text: string): void {
|
|
105
|
+
console.log(`\n${RED} Error: ${text}${RESET}\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printInfo(text: string): void {
|
|
109
|
+
console.log(`\n${GOLD} ${text}${RESET}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function printToolUsage(tools: string[]): void {
|
|
113
|
+
if (tools.length === 0) return;
|
|
114
|
+
console.log(`${DIM} tools: ${GREEN}${tools.join(', ')}${RESET}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printGatewayCommandResult(result: GatewayCommandResult): void {
|
|
118
|
+
if (result.kind === 'error') {
|
|
119
|
+
const prefix = result.title ? `${result.title}: ` : '';
|
|
120
|
+
printError(`${prefix}${result.text}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
printInfo(renderGatewayCommand(result));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function spinner(): { stop: () => void } {
|
|
127
|
+
const dots = [' ', '. ', '.. ', '...'];
|
|
128
|
+
let i = 0;
|
|
129
|
+
const clearLine = () => process.stdout.write('\r\x1b[2K');
|
|
130
|
+
const render = () => {
|
|
131
|
+
clearLine();
|
|
132
|
+
process.stdout.write(`\r ${TEAL}thinking${dots[i % dots.length]}${RESET}`);
|
|
133
|
+
i++;
|
|
134
|
+
};
|
|
135
|
+
const interval = setInterval(render, 350);
|
|
136
|
+
render();
|
|
137
|
+
return {
|
|
138
|
+
stop: () => {
|
|
139
|
+
clearInterval(interval);
|
|
140
|
+
clearLine();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runGatewayCommand(args: string[]): Promise<void> {
|
|
146
|
+
try {
|
|
147
|
+
const result = await gatewayCommand({
|
|
148
|
+
sessionId: SESSION_ID,
|
|
149
|
+
guildId: null,
|
|
150
|
+
channelId: CHANNEL_ID,
|
|
151
|
+
args,
|
|
152
|
+
});
|
|
153
|
+
printGatewayCommandResult(result);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleSlashCommand(input: string, rl: readline.Interface): Promise<boolean> {
|
|
160
|
+
const parts = input.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
161
|
+
const cmd = (parts[0] || '').toLowerCase();
|
|
162
|
+
|
|
163
|
+
switch (cmd) {
|
|
164
|
+
case 'help':
|
|
165
|
+
printHelp();
|
|
166
|
+
return true;
|
|
167
|
+
case 'exit':
|
|
168
|
+
case 'quit':
|
|
169
|
+
case 'q':
|
|
170
|
+
console.log(`\n ${GOLD}Goodbye!${RESET}\n`);
|
|
171
|
+
rl.close();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
case 'bots':
|
|
174
|
+
await runGatewayCommand(['bot', 'list']);
|
|
175
|
+
return true;
|
|
176
|
+
case 'bot':
|
|
177
|
+
if (parts.length > 1) {
|
|
178
|
+
await runGatewayCommand(['bot', 'set', ...parts.slice(1)]);
|
|
179
|
+
} else {
|
|
180
|
+
await runGatewayCommand(['bot', 'info']);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
case 'rag':
|
|
184
|
+
if (parts.length > 1 && (parts[1] === 'on' || parts[1] === 'off')) {
|
|
185
|
+
await runGatewayCommand(['rag', parts[1]]);
|
|
186
|
+
} else {
|
|
187
|
+
await runGatewayCommand(['rag']);
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
case 'info':
|
|
191
|
+
await runGatewayCommand(['bot', 'info']);
|
|
192
|
+
await runGatewayCommand(['model', 'info']);
|
|
193
|
+
await runGatewayCommand(['status']);
|
|
194
|
+
return true;
|
|
195
|
+
case 'clear':
|
|
196
|
+
await runGatewayCommand(['clear']);
|
|
197
|
+
return true;
|
|
198
|
+
case 'stop':
|
|
199
|
+
case 'abort':
|
|
200
|
+
if (activeRunAbortController && !activeRunAbortController.signal.aborted) {
|
|
201
|
+
activeRunAbortController.abort();
|
|
202
|
+
printInfo('Stopping current request...');
|
|
203
|
+
} else {
|
|
204
|
+
printInfo('No active request.');
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
default:
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function processMessage(content: string): Promise<void> {
|
|
213
|
+
const s = spinner();
|
|
214
|
+
const abortController = new AbortController();
|
|
215
|
+
activeRunAbortController = abortController;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const result = await gatewayChat(
|
|
219
|
+
{
|
|
220
|
+
sessionId: SESSION_ID,
|
|
221
|
+
guildId: null,
|
|
222
|
+
channelId: CHANNEL_ID,
|
|
223
|
+
userId: 'tui-user',
|
|
224
|
+
username: 'user',
|
|
225
|
+
content,
|
|
226
|
+
},
|
|
227
|
+
abortController.signal,
|
|
228
|
+
);
|
|
229
|
+
s.stop();
|
|
230
|
+
|
|
231
|
+
if (result.status === 'error') {
|
|
232
|
+
if ((result.error || '').includes('aborted') || (result.error || '').includes('Interrupted')) return;
|
|
233
|
+
printError(result.error || 'Unknown error');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
printToolUsage(result.toolsUsed);
|
|
238
|
+
printResponse(result.result || 'No response.');
|
|
239
|
+
} catch (err) {
|
|
240
|
+
s.stop();
|
|
241
|
+
if (abortController.signal.aborted) return;
|
|
242
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
243
|
+
} finally {
|
|
244
|
+
if (activeRunAbortController === abortController) {
|
|
245
|
+
activeRunAbortController = null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function main(): Promise<void> {
|
|
251
|
+
logger.level = 'warn';
|
|
252
|
+
await gatewayStatus();
|
|
253
|
+
printBanner();
|
|
254
|
+
|
|
255
|
+
const rl = readline.createInterface({
|
|
256
|
+
input: process.stdin,
|
|
257
|
+
output: process.stdout,
|
|
258
|
+
prompt: `${TEAL}>${RESET} `,
|
|
259
|
+
historySize: 100,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
263
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
264
|
+
process.stdin.on('keypress', (_str, key) => {
|
|
265
|
+
if (key?.name !== 'escape') return;
|
|
266
|
+
if (!activeRunAbortController || activeRunAbortController.signal.aborted) return;
|
|
267
|
+
activeRunAbortController.abort();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
rl.prompt();
|
|
271
|
+
let pendingInputLines: string[] = [];
|
|
272
|
+
let pendingInputTimer: ReturnType<typeof setTimeout> | null = null;
|
|
273
|
+
let inputRunQueue = Promise.resolve();
|
|
274
|
+
|
|
275
|
+
const enqueueInput = (input: string): void => {
|
|
276
|
+
inputRunQueue = inputRunQueue
|
|
277
|
+
.then(async () => {
|
|
278
|
+
const trimmed = input.trim();
|
|
279
|
+
if (!trimmed) {
|
|
280
|
+
rl.prompt();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!input.includes('\n') && trimmed.startsWith('/')) {
|
|
284
|
+
const handled = await handleSlashCommand(trimmed, rl);
|
|
285
|
+
if (handled) {
|
|
286
|
+
rl.prompt();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await processMessage(input);
|
|
291
|
+
rl.prompt();
|
|
292
|
+
})
|
|
293
|
+
.catch((err) => {
|
|
294
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
295
|
+
rl.prompt();
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const flushPendingInput = (): void => {
|
|
300
|
+
if (pendingInputTimer) {
|
|
301
|
+
clearTimeout(pendingInputTimer);
|
|
302
|
+
pendingInputTimer = null;
|
|
303
|
+
}
|
|
304
|
+
if (pendingInputLines.length === 0) return;
|
|
305
|
+
const combined = pendingInputLines.join('\n');
|
|
306
|
+
pendingInputLines = [];
|
|
307
|
+
enqueueInput(combined);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
rl.on('line', (line) => {
|
|
311
|
+
pendingInputLines.push(line);
|
|
312
|
+
if (pendingInputTimer) clearTimeout(pendingInputTimer);
|
|
313
|
+
pendingInputTimer = setTimeout(flushPendingInput, TUI_MULTILINE_PASTE_DEBOUNCE_MS);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
rl.on('close', () => {
|
|
317
|
+
if (pendingInputTimer) clearTimeout(pendingInputTimer);
|
|
318
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
319
|
+
console.log(`\n${DIM} Goodbye!${RESET}\n`);
|
|
320
|
+
process.exit(0);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
main().catch((err) => {
|
|
325
|
+
console.error('TUI error:', err);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// --- HybridAI API types ---
|
|
2
|
+
|
|
3
|
+
export interface ChatMessage {
|
|
4
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
5
|
+
content: string | null;
|
|
6
|
+
tool_calls?: ToolCall[];
|
|
7
|
+
tool_call_id?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolCall {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'function';
|
|
13
|
+
function: {
|
|
14
|
+
name: string;
|
|
15
|
+
arguments: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HybridAIBot {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- Container IPC types ---
|
|
26
|
+
|
|
27
|
+
export interface ContainerInput {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
messages: ChatMessage[];
|
|
30
|
+
chatbotId: string;
|
|
31
|
+
enableRag: boolean;
|
|
32
|
+
apiKey: string;
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
model: string;
|
|
35
|
+
channelId: string;
|
|
36
|
+
scheduledTasks?: { id: number; cronExpr: string; runAt: string | null; everyMs: number | null; prompt: string; enabled: number; lastRun: string | null; createdAt: string }[];
|
|
37
|
+
allowedTools?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ToolExecution {
|
|
41
|
+
name: string;
|
|
42
|
+
arguments: string;
|
|
43
|
+
result: string;
|
|
44
|
+
durationMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ToolProgressEvent {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
toolName: string;
|
|
50
|
+
phase: 'start' | 'finish';
|
|
51
|
+
preview?: string;
|
|
52
|
+
durationMs?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ContainerOutput {
|
|
56
|
+
status: 'success' | 'error';
|
|
57
|
+
result: string | null;
|
|
58
|
+
toolsUsed: string[];
|
|
59
|
+
toolExecutions?: ToolExecution[];
|
|
60
|
+
error?: string;
|
|
61
|
+
sideEffects?: { schedules?: ScheduleSideEffect[] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ScheduleSideEffect =
|
|
65
|
+
| { action: 'add'; cronExpr?: string; runAt?: string; everyMs?: number; prompt: string }
|
|
66
|
+
| { action: 'remove'; taskId: number };
|
|
67
|
+
|
|
68
|
+
// --- Database types ---
|
|
69
|
+
|
|
70
|
+
export interface Session {
|
|
71
|
+
id: string;
|
|
72
|
+
guild_id: string | null;
|
|
73
|
+
channel_id: string;
|
|
74
|
+
chatbot_id: string | null;
|
|
75
|
+
model: string | null;
|
|
76
|
+
enable_rag: number;
|
|
77
|
+
message_count: number;
|
|
78
|
+
session_summary: string | null;
|
|
79
|
+
summary_updated_at: string | null;
|
|
80
|
+
compaction_count: number;
|
|
81
|
+
memory_flush_at: string | null;
|
|
82
|
+
created_at: string;
|
|
83
|
+
last_active: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StoredMessage {
|
|
87
|
+
id: number;
|
|
88
|
+
session_id: string;
|
|
89
|
+
user_id: string;
|
|
90
|
+
username: string | null;
|
|
91
|
+
role: string;
|
|
92
|
+
content: string;
|
|
93
|
+
created_at: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ScheduledTask {
|
|
97
|
+
id: number;
|
|
98
|
+
session_id: string;
|
|
99
|
+
channel_id: string;
|
|
100
|
+
cron_expr: string;
|
|
101
|
+
run_at: string | null;
|
|
102
|
+
every_ms: number | null;
|
|
103
|
+
prompt: string;
|
|
104
|
+
enabled: number;
|
|
105
|
+
last_run: string | null;
|
|
106
|
+
created_at: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface AuditEntry {
|
|
110
|
+
id: number;
|
|
111
|
+
session_id: string | null;
|
|
112
|
+
event: string;
|
|
113
|
+
detail: string | null;
|
|
114
|
+
duration_ms: number | null;
|
|
115
|
+
created_at: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Mount security types ---
|
|
119
|
+
|
|
120
|
+
export interface AdditionalMount {
|
|
121
|
+
hostPath: string;
|
|
122
|
+
containerPath?: string;
|
|
123
|
+
readonly?: boolean; // Default: true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface MountAllowlist {
|
|
127
|
+
allowedRoots: AllowedRoot[];
|
|
128
|
+
blockedPatterns: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface AllowedRoot {
|
|
132
|
+
path: string;
|
|
133
|
+
allowReadWrite: boolean;
|
|
134
|
+
description?: string;
|
|
135
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace bootstrap files — loads SOUL.md, IDENTITY.md, USER.md,
|
|
3
|
+
* TOOLS.md, MEMORY.md, HEARTBEAT.md from the agent workspace
|
|
4
|
+
* and injects them into the system prompt (like OpenClaw).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger.js';
|
|
10
|
+
import { agentWorkspaceDir } from './ipc.js';
|
|
11
|
+
|
|
12
|
+
const BOOTSTRAP_FILES = [
|
|
13
|
+
'AGENTS.md',
|
|
14
|
+
'SOUL.md',
|
|
15
|
+
'IDENTITY.md',
|
|
16
|
+
'USER.md',
|
|
17
|
+
'TOOLS.md',
|
|
18
|
+
'MEMORY.md',
|
|
19
|
+
'HEARTBEAT.md',
|
|
20
|
+
'BOOTSTRAP.md',
|
|
21
|
+
'BOOT.md',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const MAX_FILE_CHARS = 20_000;
|
|
25
|
+
const TEMPLATES_DIR = path.join(process.cwd(), 'templates');
|
|
26
|
+
|
|
27
|
+
export interface ContextFile {
|
|
28
|
+
name: string;
|
|
29
|
+
content: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure workspace has bootstrap files, copying from templates if missing.
|
|
34
|
+
*/
|
|
35
|
+
export function ensureBootstrapFiles(agentId: string): void {
|
|
36
|
+
const wsDir = agentWorkspaceDir(agentId);
|
|
37
|
+
fs.mkdirSync(wsDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
for (const filename of BOOTSTRAP_FILES) {
|
|
40
|
+
const destPath = path.join(wsDir, filename);
|
|
41
|
+
if (fs.existsSync(destPath)) continue;
|
|
42
|
+
|
|
43
|
+
const templatePath = path.join(TEMPLATES_DIR, filename);
|
|
44
|
+
if (fs.existsSync(templatePath)) {
|
|
45
|
+
fs.copyFileSync(templatePath, destPath);
|
|
46
|
+
logger.debug({ agentId, file: filename }, 'Copied bootstrap template');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load all bootstrap files from the workspace.
|
|
53
|
+
* Returns only files that exist and have content.
|
|
54
|
+
*/
|
|
55
|
+
export function loadBootstrapFiles(agentId: string): ContextFile[] {
|
|
56
|
+
const wsDir = agentWorkspaceDir(agentId);
|
|
57
|
+
const files: ContextFile[] = [];
|
|
58
|
+
|
|
59
|
+
for (const filename of BOOTSTRAP_FILES) {
|
|
60
|
+
const filePath = path.join(wsDir, filename);
|
|
61
|
+
if (!fs.existsSync(filePath)) continue;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
let content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
65
|
+
if (!content) continue;
|
|
66
|
+
|
|
67
|
+
if (content.length > MAX_FILE_CHARS) {
|
|
68
|
+
content = content.slice(0, MAX_FILE_CHARS) + '\n\n[truncated]';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
files.push({ name: filename, content });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.warn({ agentId, file: filename, err }, 'Failed to read bootstrap file');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format the current date/time in a human-friendly way, like OpenClaw.
|
|
82
|
+
* e.g. "Tuesday, February 24th, 2026 — 14:32"
|
|
83
|
+
*/
|
|
84
|
+
function formatCurrentTime(timezone?: string): string {
|
|
85
|
+
const tz = timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
86
|
+
const now = new Date();
|
|
87
|
+
try {
|
|
88
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
89
|
+
timeZone: tz,
|
|
90
|
+
weekday: 'long',
|
|
91
|
+
year: 'numeric',
|
|
92
|
+
month: 'long',
|
|
93
|
+
day: 'numeric',
|
|
94
|
+
hour: '2-digit',
|
|
95
|
+
minute: '2-digit',
|
|
96
|
+
hourCycle: 'h23',
|
|
97
|
+
}).formatToParts(now);
|
|
98
|
+
const map: Record<string, string> = {};
|
|
99
|
+
for (const part of parts) {
|
|
100
|
+
if (part.type !== 'literal') map[part.type] = part.value;
|
|
101
|
+
}
|
|
102
|
+
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute) {
|
|
103
|
+
return now.toISOString();
|
|
104
|
+
}
|
|
105
|
+
const dayNum = parseInt(map.day, 10);
|
|
106
|
+
const suffix = dayNum >= 11 && dayNum <= 13 ? 'th'
|
|
107
|
+
: dayNum % 10 === 1 ? 'st'
|
|
108
|
+
: dayNum % 10 === 2 ? 'nd'
|
|
109
|
+
: dayNum % 10 === 3 ? 'rd' : 'th';
|
|
110
|
+
return `${map.weekday}, ${map.month} ${dayNum}${suffix}, ${map.year} — ${map.hour}:${map.minute} (${tz})`;
|
|
111
|
+
} catch {
|
|
112
|
+
return now.toISOString();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a system prompt section from loaded context files.
|
|
118
|
+
* Injects current date/time (like OpenClaw) so the agent knows when "now" is.
|
|
119
|
+
*/
|
|
120
|
+
export function buildContextPrompt(files: ContextFile[]): string {
|
|
121
|
+
if (files.length === 0) return '';
|
|
122
|
+
|
|
123
|
+
// Extract timezone from USER.md if available
|
|
124
|
+
const userFile = files.find((f) => f.name === 'USER.md');
|
|
125
|
+
const tzMatch = userFile?.content.match(/\*\*Timezone:\*\*\s*(.+)/i);
|
|
126
|
+
const userTimezone = tzMatch?.[1]?.trim() || undefined;
|
|
127
|
+
|
|
128
|
+
const lines: string[] = [
|
|
129
|
+
'# Project Context',
|
|
130
|
+
'',
|
|
131
|
+
'The following workspace context files have been loaded.',
|
|
132
|
+
'If SOUL.md is present, embody its persona and tone.',
|
|
133
|
+
'',
|
|
134
|
+
'## Current Date & Time',
|
|
135
|
+
formatCurrentTime(userTimezone),
|
|
136
|
+
'',
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
lines.push(`## ${file.name}`, '', file.content, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if the workspace still needs bootstrapping.
|
|
148
|
+
* Like OpenClaw: if the agent deleted BOOTSTRAP.md, or if IDENTITY.md / USER.md
|
|
149
|
+
* have been modified from templates, bootstrapping is considered complete.
|
|
150
|
+
*/
|
|
151
|
+
export function isBootstrapping(agentId: string): boolean {
|
|
152
|
+
const wsDir = agentWorkspaceDir(agentId);
|
|
153
|
+
if (!fs.existsSync(path.join(wsDir, 'BOOTSTRAP.md'))) return false;
|
|
154
|
+
|
|
155
|
+
// Fallback: if the agent already filled in IDENTITY.md or USER.md, it's done
|
|
156
|
+
for (const filename of ['IDENTITY.md', 'USER.md']) {
|
|
157
|
+
const wsFile = path.join(wsDir, filename);
|
|
158
|
+
const tmplFile = path.join(TEMPLATES_DIR, filename);
|
|
159
|
+
if (!fs.existsSync(wsFile) || !fs.existsSync(tmplFile)) continue;
|
|
160
|
+
const wsContent = fs.readFileSync(wsFile, 'utf-8');
|
|
161
|
+
const tmplContent = fs.readFileSync(tmplFile, 'utf-8');
|
|
162
|
+
if (wsContent !== tmplContent) {
|
|
163
|
+
// Agent modified workspace files — bootstrapping is effectively done, clean up
|
|
164
|
+
fs.unlinkSync(path.join(wsDir, 'BOOTSTRAP.md'));
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|