helixmind 0.2.15 → 0.2.17
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.
Potentially problematic release.
This version of helixmind might be problematic. Click here for more details.
- package/LICENSE +2 -2
- package/README.md +34 -107
- package/dist/cli/agent/loop.d.ts +2 -0
- package/dist/cli/agent/loop.d.ts.map +1 -1
- package/dist/cli/agent/loop.js +17 -4
- package/dist/cli/agent/loop.js.map +1 -1
- package/dist/cli/brain/template.d.ts.map +1 -1
- package/dist/cli/brain/template.js +201 -181
- package/dist/cli/brain/template.js.map +1 -1
- package/dist/cli/commands/chat.d.ts.map +1 -1
- package/dist/cli/commands/chat.js +146 -75
- package/dist/cli/commands/chat.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui/tool-output.d.ts +5 -0
- package/dist/cli/ui/tool-output.d.ts.map +1 -1
- package/dist/cli/ui/tool-output.js +23 -0
- package/dist/cli/ui/tool-output.js.map +1 -1
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ import { analyzeProject } from '../context/project.js';
|
|
|
8
8
|
import { assembleSystemPrompt } from '../context/assembler.js';
|
|
9
9
|
import { renderLogo } from '../ui/logo.js';
|
|
10
10
|
import { renderError, renderInfo, renderSpiralStatus, renderUserMessage, } from '../ui/chat-view.js';
|
|
11
|
-
import { isInsideToolBlock } from '../ui/tool-output.js';
|
|
11
|
+
import { isInsideToolBlock, renderThinkingText } from '../ui/tool-output.js';
|
|
12
12
|
import { renderFeedProgress, renderFeedSummary } from '../ui/progress.js';
|
|
13
13
|
import { ActivityIndicator } from '../ui/activity.js';
|
|
14
14
|
import { BottomChrome } from '../ui/bottom-chrome.js';
|
|
@@ -594,6 +594,9 @@ export async function chatCommand(options) {
|
|
|
594
594
|
const { buildBaseline } = await import('../agent/monitor/baseline.js');
|
|
595
595
|
const { runMonitorLoop } = await import('../agent/monitor/watcher.js');
|
|
596
596
|
const { pushMonitorStatus: pushStatus } = await import('../brain/generator.js');
|
|
597
|
+
// Helper: log to both session buffer and terminal
|
|
598
|
+
const mLog = (msg) => { bgSession.capture(msg); renderInfo(`${chalk.hex('#ff6600')(icon)} ${chalk.dim(msg)}`); };
|
|
599
|
+
mLog('Phase 1: Scanning system...');
|
|
597
600
|
const scanResult = await scanSystem({
|
|
598
601
|
sendMessage: async (prompt) => {
|
|
599
602
|
bgSession.controller.reset();
|
|
@@ -606,14 +609,17 @@ export async function chatCommand(options) {
|
|
|
606
609
|
},
|
|
607
610
|
isAborted: () => bgSession.controller.isAborted,
|
|
608
611
|
onThreat: () => { }, onDefense: () => { },
|
|
609
|
-
onScanComplete: (p) =>
|
|
612
|
+
onScanComplete: (p) => mLog(`Scan: ${p}`),
|
|
610
613
|
onStatusUpdate: () => { }, updateStatus: () => { },
|
|
611
614
|
});
|
|
612
615
|
if (bgSession.controller.isAborted)
|
|
613
616
|
return;
|
|
617
|
+
mLog('Phase 2: Building baseline...');
|
|
614
618
|
const baseline = buildBaseline(scanResult);
|
|
619
|
+
mLog(`Baseline: ${baseline.processes.length} processes, ${baseline.ports.length} ports`);
|
|
615
620
|
if (bgSession.controller.isAborted)
|
|
616
621
|
return;
|
|
622
|
+
mLog(`Phase 3: Monitoring (${mode} mode)...`);
|
|
617
623
|
await runMonitorLoop({
|
|
618
624
|
sendMessage: async (prompt) => {
|
|
619
625
|
bgSession.controller.reset();
|
|
@@ -625,9 +631,18 @@ export async function chatCommand(options) {
|
|
|
625
631
|
return rth.text;
|
|
626
632
|
},
|
|
627
633
|
isAborted: () => bgSession.controller.isAborted,
|
|
628
|
-
onThreat: (t) =>
|
|
629
|
-
|
|
630
|
-
|
|
634
|
+
onThreat: (t) => {
|
|
635
|
+
const msg = `THREAT [${t.severity}]: ${t.title}`;
|
|
636
|
+
bgSession.capture(msg);
|
|
637
|
+
const sc = t.severity === 'critical' ? '#ff0000' : t.severity === 'high' ? '#ff6600' : t.severity === 'medium' ? '#ffaa00' : '#888888';
|
|
638
|
+
renderInfo(`${chalk.hex(sc)('\u26A0')} ${chalk.hex(sc)(msg)}`);
|
|
639
|
+
},
|
|
640
|
+
onDefense: (d) => {
|
|
641
|
+
const msg = `DEFENSE: ${d.action} \u2192 ${d.target}`;
|
|
642
|
+
bgSession.capture(msg);
|
|
643
|
+
renderInfo(`${chalk.green('\u{1F6E1}\uFE0F')} ${chalk.green(msg)}`);
|
|
644
|
+
},
|
|
645
|
+
onScanComplete: (p) => mLog(`Check: ${p}`),
|
|
631
646
|
onStatusUpdate: (state) => {
|
|
632
647
|
pushStatus({ mode: state.mode, uptime: state.uptime, threatCount: state.threats.length, defenseCount: state.defenses.length, lastScan: state.lastScan });
|
|
633
648
|
},
|
|
@@ -959,10 +974,15 @@ export async function chatCommand(options) {
|
|
|
959
974
|
});
|
|
960
975
|
// Ctrl+C behavior:
|
|
961
976
|
// - If there's text on the line → clear the line (like a normal terminal)
|
|
977
|
+
// - If agent is running → interrupt agent
|
|
962
978
|
// - If line is empty → count towards exit (double Ctrl+C = exit)
|
|
979
|
+
//
|
|
980
|
+
// IMPORTANT: Use rl.on('SIGINT') instead of process.on('SIGINT') because
|
|
981
|
+
// readline clears rl.line BEFORE emitting process SIGINT. The rl-level
|
|
982
|
+
// event fires while rl.line still has the original content.
|
|
963
983
|
let ctrlCCount = 0;
|
|
964
984
|
let ctrlCTimer = null;
|
|
965
|
-
|
|
985
|
+
rl.on('SIGINT', () => {
|
|
966
986
|
// If agent is running, treat Ctrl+C as interrupt
|
|
967
987
|
if (agentRunning) {
|
|
968
988
|
activity.stop('Stopped');
|
|
@@ -973,6 +993,7 @@ export async function chatCommand(options) {
|
|
|
973
993
|
return;
|
|
974
994
|
}
|
|
975
995
|
// Check if readline has text on the current line
|
|
996
|
+
// (rl.line is still populated at this point — not yet cleared)
|
|
976
997
|
const currentLine = rl.line || '';
|
|
977
998
|
if (currentLine.length > 0) {
|
|
978
999
|
// Clear current input — write a new line and re-prompt
|
|
@@ -984,6 +1005,20 @@ export async function chatCommand(options) {
|
|
|
984
1005
|
ctrlCCount = 0;
|
|
985
1006
|
return;
|
|
986
1007
|
}
|
|
1008
|
+
// Also clear paste buffer if it has content
|
|
1009
|
+
if (pasteBuffer.length > 0) {
|
|
1010
|
+
pasteBuffer = [];
|
|
1011
|
+
if (pasteTimer) {
|
|
1012
|
+
clearTimeout(pasteTimer);
|
|
1013
|
+
pasteTimer = null;
|
|
1014
|
+
}
|
|
1015
|
+
process.stdout.write('\n');
|
|
1016
|
+
renderInfo(chalk.dim('Input cleared.'));
|
|
1017
|
+
isAtPrompt = true;
|
|
1018
|
+
rl.prompt();
|
|
1019
|
+
ctrlCCount = 0;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
987
1022
|
// Empty line — count towards exit
|
|
988
1023
|
ctrlCCount++;
|
|
989
1024
|
if (ctrlCCount >= 2) {
|
|
@@ -1008,6 +1043,13 @@ export async function chatCommand(options) {
|
|
|
1008
1043
|
clearTimeout(ctrlCTimer);
|
|
1009
1044
|
ctrlCTimer = setTimeout(() => { ctrlCCount = 0; }, 2000);
|
|
1010
1045
|
});
|
|
1046
|
+
// Fallback: OS-level SIGINT (e.g. kill -INT) — graceful exit
|
|
1047
|
+
process.on('SIGINT', () => {
|
|
1048
|
+
// Only handle if not already handled by rl.on('SIGINT')
|
|
1049
|
+
if (!process.stdin.isTTY) {
|
|
1050
|
+
process.exit(0);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1011
1053
|
/** Register brain event handlers (voice, scope switch) — reusable for auto-start and /brain */
|
|
1012
1054
|
async function registerBrainHandlers() {
|
|
1013
1055
|
const { onBrainVoiceInput, onBrainScopeSwitch, pushScopeChange, onBrainModelActivate, pushModelActivated } = await import('../brain/generator.js');
|
|
@@ -1126,8 +1168,10 @@ export async function chatCommand(options) {
|
|
|
1126
1168
|
}
|
|
1127
1169
|
}
|
|
1128
1170
|
// === ESC detection ===
|
|
1129
|
-
// Single ESC stops
|
|
1130
|
-
// Double ESC
|
|
1171
|
+
// Single ESC stops running agents immediately.
|
|
1172
|
+
// Double ESC opens the checkpoint Rewind browser.
|
|
1173
|
+
// IMPORTANT: Don't return after STOP — fall through so processKeypress
|
|
1174
|
+
// always sees the ESC for double-ESC detection.
|
|
1131
1175
|
if (key.name === 'escape') {
|
|
1132
1176
|
if (agentRunning || sessionMgr.hasBackgroundTasks || autonomousMode) {
|
|
1133
1177
|
// Clear any suggestions
|
|
@@ -1148,42 +1192,51 @@ export async function chatCommand(options) {
|
|
|
1148
1192
|
renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 All agents interrupted.'));
|
|
1149
1193
|
// Restore prompt so user can type again
|
|
1150
1194
|
showPrompt();
|
|
1151
|
-
return
|
|
1195
|
+
// NOTE: no return — fall through to double-ESC detection below
|
|
1152
1196
|
}
|
|
1153
1197
|
}
|
|
1154
|
-
// Double-ESC detection (
|
|
1198
|
+
// Double-ESC detection (checkpoint Rewind browser)
|
|
1199
|
+
// processKeypress tracks ESC timing even when STOP ran above.
|
|
1155
1200
|
const result = processKeypress(key, keyState);
|
|
1156
1201
|
if (result.action === 'open_browser' && !agentRunning) {
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1202
|
+
// Check if there are any checkpoints before opening browser
|
|
1203
|
+
const allCps = checkpointStore.getAll();
|
|
1204
|
+
if (allCps.length === 0 || allCps.filter(cp => cp.type === 'chat').length === 0) {
|
|
1205
|
+
renderInfo(chalk.dim('No checkpoints yet \u2014 start chatting to create rewind points.'));
|
|
1206
|
+
showPrompt();
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
// Open checkpoint browser — deactivate chrome for fullscreen TUI
|
|
1210
|
+
rl.pause();
|
|
1211
|
+
chrome.deactivate();
|
|
1212
|
+
try {
|
|
1213
|
+
const browserResult = await runCheckpointBrowser({
|
|
1214
|
+
store: checkpointStore,
|
|
1215
|
+
agentHistory,
|
|
1216
|
+
simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
|
|
1217
|
+
isPaused: false,
|
|
1218
|
+
});
|
|
1219
|
+
if (browserResult.action === 'revert') {
|
|
1220
|
+
const r = browserResult.result;
|
|
1221
|
+
process.stdout.write('\n');
|
|
1222
|
+
if (r.messagesRemoved > 0)
|
|
1223
|
+
renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
|
|
1224
|
+
if (r.filesReverted > 0)
|
|
1225
|
+
renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
|
|
1226
|
+
// Restore user text into readline input
|
|
1227
|
+
if (browserResult.messageText) {
|
|
1228
|
+
rl.line = browserResult.messageText;
|
|
1229
|
+
rl.cursor = browserResult.messageText.length;
|
|
1230
|
+
}
|
|
1178
1231
|
}
|
|
1179
1232
|
}
|
|
1233
|
+
catch {
|
|
1234
|
+
// Browser closed unexpectedly
|
|
1235
|
+
}
|
|
1236
|
+
chrome.activate();
|
|
1237
|
+
rl.resume();
|
|
1238
|
+
showPrompt();
|
|
1180
1239
|
}
|
|
1181
|
-
catch {
|
|
1182
|
-
// Browser closed unexpectedly
|
|
1183
|
-
}
|
|
1184
|
-
chrome.activate();
|
|
1185
|
-
rl.resume();
|
|
1186
|
-
showPrompt();
|
|
1187
1240
|
}
|
|
1188
1241
|
});
|
|
1189
1242
|
}
|
|
@@ -1440,8 +1493,13 @@ export async function chatCommand(options) {
|
|
|
1440
1493
|
const { buildBaseline } = await import('../agent/monitor/baseline.js');
|
|
1441
1494
|
const { runMonitorLoop } = await import('../agent/monitor/watcher.js');
|
|
1442
1495
|
const { pushMonitorStatus } = await import('../brain/generator.js');
|
|
1496
|
+
// Helper: show monitor event in terminal (prefixed with icon)
|
|
1497
|
+
const monitorLog = (msg) => {
|
|
1498
|
+
bgSession.capture(msg);
|
|
1499
|
+
renderInfo(`${chalk.hex('#ff6600')(icon)} ${chalk.dim(msg)}`);
|
|
1500
|
+
};
|
|
1443
1501
|
// Phase 1: Full system scan
|
|
1444
|
-
|
|
1502
|
+
monitorLog('Phase 1: Scanning system...');
|
|
1445
1503
|
const scanResult = await scanSystem({
|
|
1446
1504
|
sendMessage: async (prompt) => {
|
|
1447
1505
|
bgSession.controller.reset();
|
|
@@ -1458,20 +1516,20 @@ export async function chatCommand(options) {
|
|
|
1458
1516
|
isAborted: () => bgSession.controller.isAborted,
|
|
1459
1517
|
onThreat: () => { },
|
|
1460
1518
|
onDefense: () => { },
|
|
1461
|
-
onScanComplete: (phase) =>
|
|
1519
|
+
onScanComplete: (phase) => monitorLog(`Scan: ${phase}`),
|
|
1462
1520
|
onStatusUpdate: () => updateStatusBar(),
|
|
1463
1521
|
updateStatus: () => updateStatusBar(),
|
|
1464
1522
|
});
|
|
1465
1523
|
if (bgSession.controller.isAborted)
|
|
1466
1524
|
return;
|
|
1467
1525
|
// Phase 2: Build baseline
|
|
1468
|
-
|
|
1526
|
+
monitorLog('Phase 2: Building security baseline...');
|
|
1469
1527
|
const baseline = buildBaseline(scanResult);
|
|
1470
|
-
|
|
1528
|
+
monitorLog(`Baseline: ${baseline.processes.length} processes, ${baseline.ports.length} ports`);
|
|
1471
1529
|
if (bgSession.controller.isAborted)
|
|
1472
1530
|
return;
|
|
1473
1531
|
// Phase 3: Continuous watching
|
|
1474
|
-
|
|
1532
|
+
monitorLog(`Phase 3: Monitoring (${monitorMode} mode)...`);
|
|
1475
1533
|
await runMonitorLoop({
|
|
1476
1534
|
sendMessage: async (prompt) => {
|
|
1477
1535
|
bgSession.controller.reset();
|
|
@@ -1487,12 +1545,20 @@ export async function chatCommand(options) {
|
|
|
1487
1545
|
},
|
|
1488
1546
|
isAborted: () => bgSession.controller.isAborted,
|
|
1489
1547
|
onThreat: (threat) => {
|
|
1490
|
-
|
|
1548
|
+
const msg = `THREAT [${threat.severity}]: ${threat.title}`;
|
|
1549
|
+
bgSession.capture(msg);
|
|
1550
|
+
// Threats are always prominently shown
|
|
1551
|
+
const severityColor = threat.severity === 'critical' ? '#ff0000'
|
|
1552
|
+
: threat.severity === 'high' ? '#ff6600'
|
|
1553
|
+
: threat.severity === 'medium' ? '#ffaa00' : '#888888';
|
|
1554
|
+
renderInfo(`${chalk.hex(severityColor)('\u26A0')} ${chalk.hex(severityColor)(msg)}`);
|
|
1491
1555
|
},
|
|
1492
1556
|
onDefense: (defense) => {
|
|
1493
|
-
|
|
1557
|
+
const msg = `DEFENSE: ${defense.action} \u2192 ${defense.target}`;
|
|
1558
|
+
bgSession.capture(msg);
|
|
1559
|
+
renderInfo(`${chalk.green('\u{1F6E1}\uFE0F')} ${chalk.green(msg)}`);
|
|
1494
1560
|
},
|
|
1495
|
-
onScanComplete: (phase) =>
|
|
1561
|
+
onScanComplete: (phase) => monitorLog(`Check: ${phase}`),
|
|
1496
1562
|
onStatusUpdate: (state) => {
|
|
1497
1563
|
pushMonitorStatus({
|
|
1498
1564
|
mode: state.mode,
|
|
@@ -1720,36 +1786,10 @@ export async function chatCommand(options) {
|
|
|
1720
1786
|
lastSuggestionCount = 0;
|
|
1721
1787
|
}
|
|
1722
1788
|
const trimmed = line.trim();
|
|
1723
|
-
//
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
if (pasteTimer) {
|
|
1728
|
-
clearTimeout(pasteTimer);
|
|
1729
|
-
pasteTimer = null;
|
|
1730
|
-
}
|
|
1731
|
-
process.stdout.write(`\x1b[2K\r`);
|
|
1732
|
-
if (agentRunning) {
|
|
1733
|
-
// Queue the entire paste as a single type-ahead entry
|
|
1734
|
-
if (assembled) {
|
|
1735
|
-
typeAheadBuffer.push(assembled);
|
|
1736
|
-
const lineCount = assembled.split('\n').length;
|
|
1737
|
-
process.stdout.write(` ${theme.dim('\u23F3 Queued:')} ${chalk.cyan(`[${lineCount} Zeilen]`)} ${theme.dim(assembled.split('\n')[0].slice(0, 50))}\n`);
|
|
1738
|
-
}
|
|
1739
|
-
rl.prompt();
|
|
1740
|
-
}
|
|
1741
|
-
else {
|
|
1742
|
-
processInput(assembled);
|
|
1743
|
-
}
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
if (!trimmed) {
|
|
1747
|
-
isAtPrompt = true;
|
|
1748
|
-
rl.prompt();
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
// Paste detection: if a timer is already running, this is a continuation
|
|
1752
|
-
// (works both when agent is running and when idle)
|
|
1789
|
+
// Paste detection: if a timer is already running, this is a continuation.
|
|
1790
|
+
// ALL lines (including empty ones) are part of the paste block.
|
|
1791
|
+
// This MUST come before the empty-line-flush check so blank lines in
|
|
1792
|
+
// pasted text don't prematurely split the buffer into multiple messages.
|
|
1753
1793
|
if (pasteTimer) {
|
|
1754
1794
|
pasteBuffer.push(line);
|
|
1755
1795
|
clearTimeout(pasteTimer);
|
|
@@ -1781,6 +1821,31 @@ export async function chatCommand(options) {
|
|
|
1781
1821
|
}, PASTE_THRESHOLD_MS);
|
|
1782
1822
|
return;
|
|
1783
1823
|
}
|
|
1824
|
+
// If paste buffer has content and user pressed Enter on empty line → send it
|
|
1825
|
+
// (pasteTimer is null here, meaning the paste has ended and we're waiting for confirmation)
|
|
1826
|
+
if (!trimmed && pasteBuffer.length > 0) {
|
|
1827
|
+
const assembled = pasteBuffer.join('\n').trim();
|
|
1828
|
+
pasteBuffer = [];
|
|
1829
|
+
process.stdout.write(`\x1b[2K\r`);
|
|
1830
|
+
if (agentRunning) {
|
|
1831
|
+
// Queue the entire paste as a single type-ahead entry
|
|
1832
|
+
if (assembled) {
|
|
1833
|
+
typeAheadBuffer.push(assembled);
|
|
1834
|
+
const lineCount = assembled.split('\n').length;
|
|
1835
|
+
process.stdout.write(` ${theme.dim('\u23F3 Queued:')} ${chalk.cyan(`[${lineCount} Zeilen]`)} ${theme.dim(assembled.split('\n')[0].slice(0, 50))}\n`);
|
|
1836
|
+
}
|
|
1837
|
+
rl.prompt();
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
processInput(assembled);
|
|
1841
|
+
}
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (!trimmed) {
|
|
1845
|
+
isAtPrompt = true;
|
|
1846
|
+
rl.prompt();
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1784
1849
|
// First line — start the paste timer
|
|
1785
1850
|
pasteBuffer = [line];
|
|
1786
1851
|
pasteTimer = setTimeout(() => {
|
|
@@ -2011,6 +2076,12 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
|
|
|
2011
2076
|
if (status === 'error')
|
|
2012
2077
|
activity.setError();
|
|
2013
2078
|
},
|
|
2079
|
+
onThinkingText: (text) => {
|
|
2080
|
+
// Show intermediate LLM reasoning before tool calls
|
|
2081
|
+
activity.pauseAnimation();
|
|
2082
|
+
renderThinkingText(text);
|
|
2083
|
+
activity.resumeAnimation();
|
|
2084
|
+
},
|
|
2014
2085
|
onBeforeAnswer: () => {
|
|
2015
2086
|
activity.stop(); // Writes colorful "HelixMind Done" replacing animation
|
|
2016
2087
|
},
|