flowmind 1.5.3 → 1.5.6
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/CHANGELOG.md +31 -1
- package/README.md +2 -0
- package/README_CN.md +2 -0
- package/bin/flowmind.js +104 -118
- package/core/adapters/mcp-adapter.js +9 -8
- package/core/ai/providers/mimo.js +1 -1
- package/core/cli-ink.js +79 -0
- package/core/index.js +139 -1
- package/core/log-query-parser.js +324 -0
- package/core/providers/aliyun/dms-adapter.js +7 -35
- package/core/providers/aliyun/redis-adapter.js +4 -20
- package/core/providers/aliyun/sls-adapter.js +3 -10
- package/core/providers/friday/report-adapter.js +5 -25
- package/core/providers/yapi/yapi-adapter.js +6 -30
- package/core/providers/yuque/yuque-adapter.js +7 -35
- package/core/update-notifier.js +74 -0
- package/dashboard/app.jsx +69 -10
- package/dashboard/components/ActivityFeed.jsx +5 -4
- package/dashboard/components/DragonPanel.jsx +17 -1
- package/dashboard/components/McpStatusBar.jsx +19 -1
- package/dashboard/components/StatsRow.jsx +27 -2
- package/package.json +2 -1
- package/scripts/check-update.js +52 -0
- package/skills/auto-flow/index.js +451 -122
- package/skills/log-audit/index.js +146 -25
- package/skills/sls-log-audit/index.js +7 -30
- package/skills/yapi-sync-interface/index.js +146 -13
- package/skills/yuque-sync-design/index.js +132 -13
- package/tui/app.jsx +86 -9
- package/tui/components/ChatPanel.jsx +10 -7
- package/tui/components/DragonTotem.jsx +12 -1
- package/tui/components/Sidebar.jsx +19 -7
- package/tui/components/StatusBar.jsx +28 -1
- package/tui/format-result.js +60 -0
- package/tui/layout.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [1.5.6] - 2026-07-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added a `check-update` script and CLI command so already installed users can quickly see whether a newer npm release is available
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- TUI and dashboard now degrade gracefully on narrow or very small terminals instead of crashing on resize
|
|
10
|
+
- CLI TUI/dashboard startup now uses a shared Ink launcher with consistent stdin proxy cleanup
|
|
11
|
+
- Auto-flow remote checklist resolution now requires an exact service/pipeline descriptor match, so a neighboring service can no longer be started by index position alone
|
|
12
|
+
- Log audit now extracts quoted Chinese exception text as a query keyword, shows explicit no-records guidance, and surfaces raw MCP response summaries for TUI debugging
|
|
13
|
+
- Log audit now also tries to infer a keyword from natural-language log queries when the user forgets to add quotes
|
|
14
|
+
- Log audit and SLS log audit now share a natural-language parser for keyword, level, and time-window extraction
|
|
15
|
+
- Natural-language log queries now also infer service and project names when the user writes "项目" or "服务" in a loose sentence
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Sidebar, chat, status, and dashboard panels now switch to compact layouts when terminal width drops below the normal threshold
|
|
19
|
+
- Terminal resize handling now keeps the current session alive and shows a recovery hint instead of exiting on render errors
|
|
20
|
+
|
|
21
|
+
## [1.5.4] - 2026-07-01
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- FlowMind now resolves deferred MCP invocations from skill outputs and executes them through the registered adapter when available
|
|
25
|
+
- Auto-flow, YApi, Yuque, and log audit skills now return concise execution summaries instead of raw MCP payload shapes
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Repeated TUI submissions are now serialized to avoid multi-input crashes during fast follow-up commands
|
|
29
|
+
- CLI startup now falls back to ASCII-friendly banners in managed terminal environments that can crash on full graphics rendering
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `resource-bind` and related learned bindings now surface clearer resource summaries for follow-up skill reuse
|
|
33
|
+
- Result formatting now unwraps nested execution payloads so the TUI shows the actual output instead of intermediate envelopes
|
|
4
34
|
|
|
5
35
|
## [1.5.2] - 2026-07-01
|
|
6
36
|
|
package/README.md
CHANGED
package/README_CN.md
CHANGED
package/bin/flowmind.js
CHANGED
|
@@ -14,8 +14,10 @@ const path = require('path');
|
|
|
14
14
|
const { execSync } = require('child_process');
|
|
15
15
|
const FlowMind = require('../core');
|
|
16
16
|
const HonorEngine = require('../core/honor-engine');
|
|
17
|
+
const { main: runUpdateCheck } = require('../scripts/check-update');
|
|
18
|
+
const { launchInkApp } = require('../core/cli-ink');
|
|
17
19
|
const { syncSddAgentToFlowMind } = require('../core/sdd-agent-sync');
|
|
18
|
-
const { detectManagedCliHost, shouldUseAsciiUi,
|
|
20
|
+
const { detectManagedCliHost, shouldUseAsciiUi, getNestedTuiGuardMessage } = require('../core/utils');
|
|
19
21
|
const { isExitCommand } = require('../tui/ui');
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -75,7 +77,19 @@ async function initFlowMind(options = {}) {
|
|
|
75
77
|
/**
|
|
76
78
|
* Display banner
|
|
77
79
|
*/
|
|
78
|
-
function showBanner() {
|
|
80
|
+
function showBanner(asciiMode = false) {
|
|
81
|
+
if (asciiMode) {
|
|
82
|
+
console.log(chalk.cyan(`
|
|
83
|
+
+--------------------------------------------------+
|
|
84
|
+
| |
|
|
85
|
+
| FlowMind |
|
|
86
|
+
| The AI Agent That Learns How You Work |
|
|
87
|
+
| |
|
|
88
|
+
+--------------------------------------------------+
|
|
89
|
+
`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
console.log(chalk.cyan(`
|
|
80
94
|
╔══════════════════════════════════════════════════╗
|
|
81
95
|
║ ║
|
|
@@ -89,7 +103,71 @@ function showBanner() {
|
|
|
89
103
|
/**
|
|
90
104
|
* Dragon Totem ASCII Art by level - Chinese Dragon (中国龙)
|
|
91
105
|
*/
|
|
92
|
-
function getDragonArt(level) {
|
|
106
|
+
function getDragonArt(level, asciiMode = false) {
|
|
107
|
+
if (asciiMode) {
|
|
108
|
+
const asciiArts = {
|
|
109
|
+
0: [
|
|
110
|
+
' /-----\\ ',
|
|
111
|
+
' / /-\\ \\ ',
|
|
112
|
+
' | | o | | ',
|
|
113
|
+
' | | | | ',
|
|
114
|
+
' | \\_/ | ',
|
|
115
|
+
' \\ / ',
|
|
116
|
+
' \\-----/ ',
|
|
117
|
+
' Dragon Egg '
|
|
118
|
+
],
|
|
119
|
+
1: [
|
|
120
|
+
' /--\\ ',
|
|
121
|
+
' /----/ \\---\\ ',
|
|
122
|
+
' / o \\/ \\ ',
|
|
123
|
+
' \\ /\\ /\\ / ',
|
|
124
|
+
' \\/\\/ \\/\\/ \\/ ',
|
|
125
|
+
' Hatchling Dragon '
|
|
126
|
+
],
|
|
127
|
+
2: [
|
|
128
|
+
' /-\\ /-\\ ',
|
|
129
|
+
' /----/ \\/ \\---\\ ',
|
|
130
|
+
' / o __ \\ ',
|
|
131
|
+
' \\ /--------\\ / ',
|
|
132
|
+
' \\---/ ////// \\/ ',
|
|
133
|
+
' \\_/ \\_/ ',
|
|
134
|
+
' Juvenile Dragon '
|
|
135
|
+
],
|
|
136
|
+
3: [
|
|
137
|
+
' /---\\ /---\\ ',
|
|
138
|
+
' /---/ \\/ \\---\\ ',
|
|
139
|
+
'/ o __ \\ ',
|
|
140
|
+
'| /----------\\ | ',
|
|
141
|
+
' \\--/ //////// \\-/ ',
|
|
142
|
+
' \\/ ////////// \\/ ',
|
|
143
|
+
' \\_/ \\_/ ',
|
|
144
|
+
' Adult Dragon '
|
|
145
|
+
],
|
|
146
|
+
4: [
|
|
147
|
+
' /---\\ /---\\ ',
|
|
148
|
+
'/---/ \\____/ \\---\\ ',
|
|
149
|
+
'| o __ | ',
|
|
150
|
+
'| /------------\\ | ',
|
|
151
|
+
' \\-/ //////////// \\-/ ',
|
|
152
|
+
' / //////////// \\ ',
|
|
153
|
+
' /_/ \\_\\ ',
|
|
154
|
+
' Elder Dragon '
|
|
155
|
+
],
|
|
156
|
+
5: [
|
|
157
|
+
' * /---\\ /---\\ * ',
|
|
158
|
+
'/-/ \\______/ \\-\\ ',
|
|
159
|
+
'| o __ | ',
|
|
160
|
+
'| /--------------\\ | ',
|
|
161
|
+
' \\-/ /////**///// \\-/ ',
|
|
162
|
+
' / ///// ///// \\ ',
|
|
163
|
+
' /_/ ** ** \\_\\ ',
|
|
164
|
+
' * \\______________/ * ',
|
|
165
|
+
' Ascended Dragon '
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
return asciiArts[level] || asciiArts[0];
|
|
169
|
+
}
|
|
170
|
+
|
|
93
171
|
const arts = {
|
|
94
172
|
0: [
|
|
95
173
|
' ╭─────╮ ',
|
|
@@ -192,11 +270,11 @@ function getHighlightColor(level) {
|
|
|
192
270
|
/**
|
|
193
271
|
* Render dragon totem with honor data
|
|
194
272
|
*/
|
|
195
|
-
function renderDragonTotem(honorData) {
|
|
273
|
+
function renderDragonTotem(honorData, asciiMode = false) {
|
|
196
274
|
const info = getLevelInfo(honorData.points);
|
|
197
275
|
const dragonColor = getDragonColor(info.level);
|
|
198
276
|
const highlightColor = getHighlightColor(info.level);
|
|
199
|
-
const art = getDragonArt(info.level);
|
|
277
|
+
const art = getDragonArt(info.level, asciiMode);
|
|
200
278
|
|
|
201
279
|
// Calculate max art line width for proper padding
|
|
202
280
|
const maxArtWidth = Math.max(...art.map(l => l.length));
|
|
@@ -210,7 +288,8 @@ function renderDragonTotem(honorData) {
|
|
|
210
288
|
|
|
211
289
|
console.log('');
|
|
212
290
|
console.log(chalk.cyan(' ┌' + border + '┐'));
|
|
213
|
-
|
|
291
|
+
const title = asciiMode ? ' Dragon Totem of Honor ' : ' 🐉 Dragon Totem of Honor ';
|
|
292
|
+
console.log(chalk.cyan(' │') + title + padRight(title, boxWidth - 2) + chalk.cyan('│'));
|
|
214
293
|
console.log(chalk.cyan(' ├' + border + '┤'));
|
|
215
294
|
|
|
216
295
|
for (const line of art) {
|
|
@@ -366,7 +445,7 @@ program
|
|
|
366
445
|
.description('Initialize FlowMind in current directory')
|
|
367
446
|
.option('--ai <provider>', 'Initialize with AI provider (openai/anthropic/glm/mimo/qwen/ernie/deepseek/ollama)')
|
|
368
447
|
.action(async (options) => {
|
|
369
|
-
showBanner();
|
|
448
|
+
showBanner(options.ascii || shouldUseAsciiUi());
|
|
370
449
|
|
|
371
450
|
const spinner = ora('Initializing FlowMind...').start();
|
|
372
451
|
|
|
@@ -836,7 +915,7 @@ program
|
|
|
836
915
|
} else if (options.publish) {
|
|
837
916
|
await publishHonor({ output: options.output, gist: options.gist });
|
|
838
917
|
} else {
|
|
839
|
-
renderDragonTotem(honorData);
|
|
918
|
+
renderDragonTotem(honorData, shouldUseAsciiUi());
|
|
840
919
|
console.log(chalk.gray(` ${getNextLevelHint(honorData.points)}`));
|
|
841
920
|
}
|
|
842
921
|
} catch (error) {
|
|
@@ -959,7 +1038,7 @@ program
|
|
|
959
1038
|
|
|
960
1039
|
// Interactive mode
|
|
961
1040
|
async function runInteractiveMode(fm) {
|
|
962
|
-
showBanner();
|
|
1041
|
+
showBanner(shouldUseAsciiUi());
|
|
963
1042
|
console.log(chalk.cyan('Interactive mode started. Type "exit" to quit.\n'));
|
|
964
1043
|
|
|
965
1044
|
try {
|
|
@@ -1529,8 +1608,6 @@ program
|
|
|
1529
1608
|
.description('Launch enhanced TUI with split panels, skill browser, and dragon display')
|
|
1530
1609
|
.option('--ascii', 'Force ASCII-safe terminal rendering')
|
|
1531
1610
|
.action(async (options) => {
|
|
1532
|
-
let stdinWrapper = null;
|
|
1533
|
-
let stdinForwarder = null;
|
|
1534
1611
|
try {
|
|
1535
1612
|
const managedHost = detectManagedCliHost();
|
|
1536
1613
|
if (managedHost) {
|
|
@@ -1540,71 +1617,21 @@ program
|
|
|
1540
1617
|
return;
|
|
1541
1618
|
}
|
|
1542
1619
|
|
|
1543
|
-
// Register .jsx extension for CJS
|
|
1544
|
-
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
1545
|
-
|
|
1546
1620
|
const React = require('react');
|
|
1547
|
-
const { render } = require('ink');
|
|
1548
|
-
const { PassThrough } = require('stream');
|
|
1549
1621
|
const App = require('../tui/app.jsx');
|
|
1550
1622
|
const asciiMode = options.ascii || shouldUseAsciiUi();
|
|
1551
1623
|
|
|
1552
1624
|
const fm = await initFlowMind();
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
// Only proxy stdin when the host stream cannot satisfy Ink's raw-mode requirements.
|
|
1558
|
-
if (shouldProxyInkStdin(realStdin)) {
|
|
1559
|
-
stdinWrapper = new PassThrough();
|
|
1560
|
-
stdinWrapper.isTTY = true;
|
|
1561
|
-
stdinWrapper.isRaw = false;
|
|
1562
|
-
stdinWrapper.setRawMode = (mode) => {
|
|
1563
|
-
try {
|
|
1564
|
-
if (realStdin.setRawMode) {
|
|
1565
|
-
realStdin.setRawMode(mode);
|
|
1566
|
-
}
|
|
1567
|
-
} catch (e) {
|
|
1568
|
-
// Suppress raw mode errors in non-TTY environments
|
|
1569
|
-
}
|
|
1570
|
-
return stdinWrapper;
|
|
1571
|
-
};
|
|
1572
|
-
// Forward real stdin data to the wrapper (store reference for cleanup)
|
|
1573
|
-
if (realStdin.readable) {
|
|
1574
|
-
stdinForwarder = (chunk) => {
|
|
1575
|
-
if (!stdinWrapper.destroyed) {
|
|
1576
|
-
try {
|
|
1577
|
-
stdinWrapper.write(chunk);
|
|
1578
|
-
} catch (e) {
|
|
1579
|
-
// Ignore write-after-destroy errors
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
};
|
|
1583
|
-
realStdin.on('data', stdinForwarder);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
inkStdin = stdinWrapper;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
const { unmount, waitUntilExit } = render(
|
|
1590
|
-
React.createElement(App, { flowmind: fm, asciiMode: asciiMode }),
|
|
1591
|
-
{ stdin: inkStdin }
|
|
1592
|
-
);
|
|
1593
|
-
await waitUntilExit();
|
|
1594
|
-
unmount();
|
|
1625
|
+
await launchInkApp({
|
|
1626
|
+
element: React.createElement(App, { flowmind: fm, asciiMode: asciiMode }),
|
|
1627
|
+
stdin: process.stdin
|
|
1628
|
+
});
|
|
1595
1629
|
} catch (error) {
|
|
1596
1630
|
console.error(chalk.red('TUI Error:'), error.message);
|
|
1597
1631
|
if (error.message.includes('Cannot find module')) {
|
|
1598
1632
|
console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
|
|
1599
1633
|
}
|
|
1600
1634
|
} finally {
|
|
1601
|
-
// Clean up stdin listener to prevent leak
|
|
1602
|
-
if (stdinForwarder) {
|
|
1603
|
-
process.stdin.removeListener('data', stdinForwarder);
|
|
1604
|
-
}
|
|
1605
|
-
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1606
|
-
stdinWrapper.destroy();
|
|
1607
|
-
}
|
|
1608
1635
|
restoreTerminal();
|
|
1609
1636
|
}
|
|
1610
1637
|
});
|
|
@@ -1615,8 +1642,6 @@ program
|
|
|
1615
1642
|
.description('Launch real-time monitoring dashboard for MCP activity and events')
|
|
1616
1643
|
.option('--ascii', 'Force ASCII-safe terminal rendering')
|
|
1617
1644
|
.action(async (options) => {
|
|
1618
|
-
let stdinWrapper = null;
|
|
1619
|
-
let stdinForwarder = null;
|
|
1620
1645
|
try {
|
|
1621
1646
|
const managedHost = detectManagedCliHost();
|
|
1622
1647
|
if (managedHost) {
|
|
@@ -1626,68 +1651,22 @@ program
|
|
|
1626
1651
|
return;
|
|
1627
1652
|
}
|
|
1628
1653
|
|
|
1629
|
-
// Register .jsx extension for CJS
|
|
1630
|
-
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
1631
|
-
|
|
1632
1654
|
const React = require('react');
|
|
1633
|
-
const { render } = require('ink');
|
|
1634
|
-
const { PassThrough } = require('stream');
|
|
1635
1655
|
const DashboardApp = require('../dashboard/app.jsx');
|
|
1636
1656
|
const eventBus = require('../core/event-bus');
|
|
1637
1657
|
const asciiMode = options.ascii || shouldUseAsciiUi();
|
|
1638
1658
|
|
|
1639
1659
|
const fm = await initFlowMind();
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
// Only proxy stdin when the host stream cannot satisfy Ink's raw-mode requirements.
|
|
1645
|
-
if (shouldProxyInkStdin(realStdin)) {
|
|
1646
|
-
stdinWrapper = new PassThrough();
|
|
1647
|
-
stdinWrapper.isTTY = true;
|
|
1648
|
-
stdinWrapper.isRaw = false;
|
|
1649
|
-
stdinWrapper.setRawMode = (mode) => {
|
|
1650
|
-
try {
|
|
1651
|
-
if (realStdin.setRawMode) {
|
|
1652
|
-
realStdin.setRawMode(mode);
|
|
1653
|
-
}
|
|
1654
|
-
} catch (e) {
|
|
1655
|
-
// Suppress raw mode errors in non-TTY environments
|
|
1656
|
-
}
|
|
1657
|
-
return stdinWrapper;
|
|
1658
|
-
};
|
|
1659
|
-
if (realStdin.readable) {
|
|
1660
|
-
stdinForwarder = (chunk) => {
|
|
1661
|
-
if (!stdinWrapper.destroyed) {
|
|
1662
|
-
try {
|
|
1663
|
-
stdinWrapper.write(chunk);
|
|
1664
|
-
} catch (e) { /* ignore write-after-destroy */ }
|
|
1665
|
-
}
|
|
1666
|
-
};
|
|
1667
|
-
realStdin.on('data', stdinForwarder);
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
inkStdin = stdinWrapper;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
const { unmount, waitUntilExit } = render(
|
|
1674
|
-
React.createElement(DashboardApp, { flowmind: fm, eventBus, asciiMode: asciiMode }),
|
|
1675
|
-
{ stdin: inkStdin }
|
|
1676
|
-
);
|
|
1677
|
-
await waitUntilExit();
|
|
1678
|
-
unmount();
|
|
1660
|
+
await launchInkApp({
|
|
1661
|
+
element: React.createElement(DashboardApp, { flowmind: fm, eventBus, asciiMode: asciiMode }),
|
|
1662
|
+
stdin: process.stdin
|
|
1663
|
+
});
|
|
1679
1664
|
} catch (error) {
|
|
1680
1665
|
console.error(chalk.red('Dashboard Error:'), error.message);
|
|
1681
1666
|
if (error.message.includes('Cannot find module')) {
|
|
1682
1667
|
console.log(chalk.yellow('Try running: npm install ink@3 react'));
|
|
1683
1668
|
}
|
|
1684
1669
|
} finally {
|
|
1685
|
-
if (stdinForwarder) {
|
|
1686
|
-
process.stdin.removeListener('data', stdinForwarder);
|
|
1687
|
-
}
|
|
1688
|
-
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1689
|
-
stdinWrapper.destroy();
|
|
1690
|
-
}
|
|
1691
1670
|
restoreTerminal();
|
|
1692
1671
|
}
|
|
1693
1672
|
});
|
|
@@ -1813,10 +1792,17 @@ program
|
|
|
1813
1792
|
}
|
|
1814
1793
|
});
|
|
1815
1794
|
|
|
1795
|
+
program
|
|
1796
|
+
.command('check-update')
|
|
1797
|
+
.description('Check whether a newer FlowMind version is available')
|
|
1798
|
+
.action(async () => {
|
|
1799
|
+
await runUpdateCheck();
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1816
1802
|
// Parse arguments
|
|
1817
1803
|
const knownCommands = new Set([
|
|
1818
1804
|
'init', 'process', 'p', 'learn', 'scenes', 'skills', 'skill', 'resource', 'res',
|
|
1819
|
-
'stats', 'honor', 'config', 'ai', 'tui', 'dashboard', 'doctor', 'update', 'help'
|
|
1805
|
+
'stats', 'honor', 'config', 'ai', 'tui', 'dashboard', 'doctor', 'update', 'check-update', 'help'
|
|
1820
1806
|
]);
|
|
1821
1807
|
const cliArgs = process.argv.slice(2);
|
|
1822
1808
|
|
|
@@ -66,15 +66,10 @@ class McpAdapter extends BaseAdapter {
|
|
|
66
66
|
return this.config.transport || this.config.mcpServerConfig || this.config.mcpTransport || {};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
async
|
|
70
|
-
const tool = this.resolveTool(localName);
|
|
71
|
-
if (!tool) {
|
|
72
|
-
throw new Error(`Unknown MCP tool mapping: ${localName}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
69
|
+
async callTool(tool, args = {}) {
|
|
75
70
|
const transport = this.getTransportConfig();
|
|
76
71
|
const serverName = this.mcpServer || this.providerName;
|
|
77
|
-
|
|
72
|
+
return callMcpTool({
|
|
78
73
|
url: transport.url || transport.endpoint || transport.baseUrl,
|
|
79
74
|
headers: transport.headers || {},
|
|
80
75
|
tool,
|
|
@@ -82,8 +77,14 @@ class McpAdapter extends BaseAdapter {
|
|
|
82
77
|
timeoutMs: transport.timeoutMs || 60000,
|
|
83
78
|
serverName
|
|
84
79
|
});
|
|
80
|
+
}
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
async callMcpTool(localName, args = {}) {
|
|
83
|
+
const tool = this.resolveTool(localName);
|
|
84
|
+
if (!tool) {
|
|
85
|
+
throw new Error(`Unknown MCP tool mapping: ${localName}`);
|
|
86
|
+
}
|
|
87
|
+
return this.callTool(tool, args);
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
package/core/cli-ink.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { PassThrough } = require('stream');
|
|
2
|
+
const { shouldProxyInkStdin } = require('./utils');
|
|
3
|
+
|
|
4
|
+
function registerJsxExtension() {
|
|
5
|
+
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createInkStdinProxy(realStdin) {
|
|
9
|
+
if (!realStdin) {
|
|
10
|
+
return { stdin: null, wrapper: null, forwarder: null };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const wrapper = new PassThrough();
|
|
14
|
+
wrapper.isTTY = true;
|
|
15
|
+
wrapper.isRaw = false;
|
|
16
|
+
wrapper.setRawMode = (mode) => {
|
|
17
|
+
try {
|
|
18
|
+
if (realStdin.setRawMode) {
|
|
19
|
+
realStdin.setRawMode(mode);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Ignore raw mode failures in wrapped environments.
|
|
23
|
+
}
|
|
24
|
+
return wrapper;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let forwarder = null;
|
|
28
|
+
if (realStdin.readable) {
|
|
29
|
+
forwarder = (chunk) => {
|
|
30
|
+
if (!wrapper.destroyed) {
|
|
31
|
+
try {
|
|
32
|
+
wrapper.write(chunk);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Ignore write-after-destroy errors while shutting down.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
realStdin.on('data', forwarder);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { stdin: wrapper, wrapper, forwarder };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cleanupInkStdinProxy(realStdin, proxy) {
|
|
45
|
+
if (!proxy) return;
|
|
46
|
+
|
|
47
|
+
if (proxy.forwarder && realStdin?.removeListener) {
|
|
48
|
+
realStdin.removeListener('data', proxy.forwarder);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (proxy.wrapper && !proxy.wrapper.destroyed) {
|
|
52
|
+
proxy.wrapper.destroy();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function launchInkApp({ element, stdin = process.stdin }) {
|
|
57
|
+
registerJsxExtension();
|
|
58
|
+
const { render } = require('ink');
|
|
59
|
+
const proxy = shouldProxyInkStdin(stdin) ? createInkStdinProxy(stdin) : { stdin: null, wrapper: null, forwarder: null };
|
|
60
|
+
let unmount = null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const rendered = render(element, { stdin: proxy.stdin || stdin });
|
|
64
|
+
unmount = rendered.unmount;
|
|
65
|
+
await rendered.waitUntilExit();
|
|
66
|
+
} finally {
|
|
67
|
+
if (typeof unmount === 'function') {
|
|
68
|
+
unmount();
|
|
69
|
+
}
|
|
70
|
+
cleanupInkStdinProxy(stdin, proxy);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
cleanupInkStdinProxy,
|
|
76
|
+
createInkStdinProxy,
|
|
77
|
+
launchInkApp,
|
|
78
|
+
registerJsxExtension
|
|
79
|
+
};
|
package/core/index.js
CHANGED
|
@@ -217,6 +217,7 @@ class FlowMind {
|
|
|
217
217
|
|
|
218
218
|
// Execute skill
|
|
219
219
|
const result = await skill.execute(input, enhancedContext);
|
|
220
|
+
const resolvedResult = await this.resolveDeferredMcpResult(result, skill, enhancedContext);
|
|
220
221
|
|
|
221
222
|
if (resourceBinding?.id) {
|
|
222
223
|
await this.learning.markResourceBindingUsed(resourceBinding.id, {
|
|
@@ -239,7 +240,144 @@ class FlowMind {
|
|
|
239
240
|
// Award honor point for skill use
|
|
240
241
|
await this.honor.award('skill_use', `Used skill: ${skill.name}`);
|
|
241
242
|
|
|
242
|
-
return
|
|
243
|
+
return resolvedResult;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async resolveDeferredMcpResult(result, skill, context = {}) {
|
|
247
|
+
const invocation = this.extractMcpInvocation(result);
|
|
248
|
+
if (!invocation) {
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const adapter = this.resolveDeferredAdapter(invocation, skill, context);
|
|
253
|
+
if (!adapter) {
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const executor = typeof adapter.callTool === 'function'
|
|
258
|
+
? adapter.callTool.bind(adapter)
|
|
259
|
+
: typeof adapter.callMcpTool === 'function'
|
|
260
|
+
? adapter.callMcpTool.bind(adapter)
|
|
261
|
+
: null;
|
|
262
|
+
|
|
263
|
+
if (!executor) {
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const execution = await executor(invocation.tool, invocation.args || {});
|
|
269
|
+
const resultData = result && typeof result === 'object' && !Array.isArray(result) && result.data && typeof result.data === 'object'
|
|
270
|
+
? result.data
|
|
271
|
+
: {};
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
...result,
|
|
275
|
+
data: {
|
|
276
|
+
...resultData,
|
|
277
|
+
execution,
|
|
278
|
+
mcpTool: invocation.tool,
|
|
279
|
+
mcpExecuted: true,
|
|
280
|
+
mcpServer: invocation.mcpServer || adapter.mcpServer || adapter.providerName
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return {
|
|
285
|
+
...result,
|
|
286
|
+
type: result?.type || 'error',
|
|
287
|
+
success: false,
|
|
288
|
+
message: result?.message || error.message,
|
|
289
|
+
data: {
|
|
290
|
+
...(result && typeof result === 'object' && result.data && typeof result.data === 'object' ? result.data : {}),
|
|
291
|
+
mcpTool: invocation.tool,
|
|
292
|
+
mcpExecuted: false,
|
|
293
|
+
mcpError: error.message
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
extractMcpInvocation(result) {
|
|
300
|
+
if (!result || typeof result !== 'object') {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const candidates = [
|
|
305
|
+
result.data,
|
|
306
|
+
result
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
for (const candidate of candidates) {
|
|
310
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const tool = candidate.mcpTool || candidate.tool;
|
|
315
|
+
if (!tool || typeof tool !== 'string') {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const args = candidate.args || candidate.params || {};
|
|
320
|
+
if (args && typeof args !== 'object') {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
tool,
|
|
326
|
+
args,
|
|
327
|
+
mcpServer: candidate.mcpServer || null
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
resolveDeferredAdapter(invocation, skill, context = {}) {
|
|
335
|
+
const registry = context.componentRegistry || this.components;
|
|
336
|
+
if (!registry) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const bindingComponentType = context.resourceBinding?.componentType || context.resolvedBinding?.componentType || null;
|
|
341
|
+
const skillComponentType = this.inferSkillComponentType(skill, context);
|
|
342
|
+
const componentTypes = [
|
|
343
|
+
bindingComponentType,
|
|
344
|
+
skillComponentType,
|
|
345
|
+
invocation.componentType
|
|
346
|
+
].filter(Boolean);
|
|
347
|
+
|
|
348
|
+
for (const componentType of componentTypes) {
|
|
349
|
+
const adapter = registry.getAdapter(componentType);
|
|
350
|
+
if (adapter) {
|
|
351
|
+
return adapter;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (typeof registry.getAll === 'function') {
|
|
356
|
+
const allAdapters = registry.getAll();
|
|
357
|
+
for (const entry of allAdapters) {
|
|
358
|
+
const adapter = registry.getAdapterByProvider?.(entry.type, entry.provider) || null;
|
|
359
|
+
if (adapter && (adapter.mcpServer === invocation.mcpServer || adapter.providerName === invocation.provider)) {
|
|
360
|
+
return adapter;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
inferSkillComponentType(skill, context = {}) {
|
|
369
|
+
const name = skill?.name || context.currentSkill || '';
|
|
370
|
+
const mapping = {
|
|
371
|
+
'log-audit': 'logService',
|
|
372
|
+
'sls-log-audit': 'logService',
|
|
373
|
+
'yapi-sync-interface': 'apiDoc',
|
|
374
|
+
'yuque-sync-design': 'knowledgeBase',
|
|
375
|
+
'data-logic-validation': 'databaseQuery',
|
|
376
|
+
'resource-bind': null,
|
|
377
|
+
'auto-flow': 'workflow'
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
return mapping[name] || null;
|
|
243
381
|
}
|
|
244
382
|
|
|
245
383
|
/**
|