flowmind 1.4.8 → 1.5.0

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.5.0] - 2026-07-01
6
+
7
+ ### Added
8
+ - `resource --sync-sdd-agent` now imports local `sdd-agent` bindings, scenes, and component state into FlowMind without touching remote services
9
+ - Passive learning can now auto-promote repeated resource-binding choices into reusable skill preferences after enough successful local runs
10
+ - Passive scene learning can now turn repeated successful scene executions into reusable default scene preferences
11
+ - `resource-bind` can now save business-specific MCP, address, token, namespace, and project bindings into local learning files
12
+ - Runtime context now injects matched learned bindings so follow-up skills can reuse the saved business connection automatically
13
+
14
+ ### Changed
15
+ - The CLI now treats direct text input as `process`, so `flowmind "..."` works without manually adding the subcommand
16
+ - Resource overview output now exposes synced bindings, scene counts, and component activation state more clearly
17
+ - Default config bootstrapping now seeds built-in values before reading local files, including passive learning thresholds
18
+
19
+ ### Fixed
20
+ - Binding-style requests such as `绑定发布平台 mcp=friday-auto-flow token=xxx env=uat` now route through `resource-bind` reliably instead of being swallowed by execution skills
21
+ - Startup now avoids a false `Missing required field: version` warning when local config files do not exist yet
22
+ - `flowmind tui` and `flowmind dashboard` now refuse to start inside managed Codex/Claude-style CLI hosts where nested fullscreen input can terminate the outer session
23
+
3
24
  ## [1.4.8] - 2026-06-30
4
25
 
5
26
  ### Changed
package/bin/flowmind.js CHANGED
@@ -14,6 +14,9 @@ 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 { syncSddAgentToFlowMind } = require('../core/sdd-agent-sync');
18
+ const { detectManagedCliHost, shouldUseAsciiUi, shouldProxyInkStdin, getNestedTuiGuardMessage } = require('../core/utils');
19
+ const { isExitCommand } = require('../tui/ui');
17
20
 
18
21
  /**
19
22
  * Restore terminal to sane state (cancel raw mode, show cursor, reset colors)
@@ -714,19 +717,32 @@ program
714
717
  program
715
718
  .command('resource')
716
719
  .alias('res')
717
- .description('Manage local resource files')
720
+ .description('Manage resource connections, learned bindings, and local resource files')
718
721
  .option('-l, --list [dir]', 'List resource directory')
719
722
  .option('-s, --show <file>', 'Show file content')
720
723
  .option('-e, --edit <file>', 'Edit file')
721
724
  .option('-c, --config', 'Show resource configuration')
725
+ .option('--sync-sdd-agent [dir]', 'Sync local SDD-Agent resources and scenes into FlowMind')
722
726
  .option('-j, --json', 'Output as JSON (for tool integration)')
723
727
  .action(async (options) => {
724
728
  try {
725
729
  const fm = await initFlowMind();
726
730
  const resourceConfig = fm.config.get('resources', {});
727
731
  const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.flowmind');
732
+ const componentStatus = fm.getComponentStatus ? fm.getComponentStatus() : {};
733
+ const bindings = await fm.learning.listResourceBindings();
728
734
 
729
- if (options.config) {
735
+ if (options.syncSddAgent !== undefined) {
736
+ const summary = await syncSddAgentToFlowMind({
737
+ sourceDir: typeof options.syncSddAgent === 'string' ? options.syncSddAgent : undefined
738
+ });
739
+
740
+ if (options.json) {
741
+ console.log(JSON.stringify({ synced: summary }, null, 2));
742
+ } else {
743
+ displayResourceSyncSummary(summary);
744
+ }
745
+ } else if (options.config) {
730
746
  // Show resource configuration
731
747
  if (options.json) {
732
748
  console.log(JSON.stringify({ resources: resourceConfig }, null, 2));
@@ -752,7 +768,7 @@ program
752
768
  // Edit file
753
769
  const filePath = resolveResourcePath(options.edit, configDir);
754
770
  await openInEditor(filePath);
755
- } else {
771
+ } else if (options.list !== undefined) {
756
772
  // List directory (default)
757
773
  const targetDir = options.list && typeof options.list === 'string'
758
774
  ? resolveResourcePath(options.list, configDir)
@@ -768,6 +784,20 @@ program
768
784
  } else {
769
785
  console.error(chalk.red(`Directory not found: ${targetDir}`));
770
786
  }
787
+ } else if (options.json) {
788
+ console.log(JSON.stringify({
789
+ resources: resourceConfig,
790
+ componentStatus,
791
+ bindings,
792
+ examples: getResourceSetupExamples()
793
+ }, null, 2));
794
+ } else {
795
+ displayResourceOverview({
796
+ resourceConfig,
797
+ componentStatus,
798
+ bindings,
799
+ configDir
800
+ });
771
801
  }
772
802
  } catch (error) {
773
803
  console.error(chalk.red('Error:'), error.message);
@@ -951,7 +981,7 @@ async function runInteractiveMode(fm) {
951
981
  break;
952
982
  }
953
983
 
954
- if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
984
+ if (isExitCommand(input)) {
955
985
  console.log(chalk.cyan('\nGoodbye! FlowMind will remember your preferences. 👋\n'));
956
986
  break;
957
987
  }
@@ -1337,6 +1367,99 @@ function displayResourceConfig(config) {
1337
1367
  console.log(chalk.cyan('└─────────────────────────────────────────────────────┘'));
1338
1368
  }
1339
1369
 
1370
+ function getResourceSetupExamples() {
1371
+ return [
1372
+ 'flowmind resource',
1373
+ 'flowmind resource --config',
1374
+ 'flowmind "绑定零售业务 mcp=yapi-mcp 地址=https://yapi.example.com token=xxx project=retail"',
1375
+ 'flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"',
1376
+ 'flowmind "同步零售业务接口到 YApi"',
1377
+ 'flowmind "部署零售服务到 UAT"'
1378
+ ];
1379
+ }
1380
+
1381
+ function maskSecret(value) {
1382
+ if (!value) return null;
1383
+ const stringValue = String(value);
1384
+ if (stringValue.length <= 6) return '***';
1385
+ return `${stringValue.slice(0, 3)}***${stringValue.slice(-2)}`;
1386
+ }
1387
+
1388
+ function describeConnection(binding) {
1389
+ const parts = [];
1390
+ if (binding.mcpServer) parts.push(`mcp=${binding.mcpServer}`);
1391
+ if (binding.connection?.address) parts.push(`address=${binding.connection.address}`);
1392
+ if (binding.connection?.project) parts.push(`project=${binding.connection.project}`);
1393
+ if (binding.connection?.namespace) parts.push(`namespace=${binding.connection.namespace}`);
1394
+ if (binding.connection?.env) parts.push(`env=${binding.connection.env}`);
1395
+ if (binding.connection?.token) parts.push(`token=${maskSecret(binding.connection.token)}`);
1396
+ return parts;
1397
+ }
1398
+
1399
+ function displayResourceOverview({ resourceConfig, componentStatus, bindings, configDir }) {
1400
+ const configuredResources = Object.keys(resourceConfig || {});
1401
+ const activeComponents = Object.entries(componentStatus || {}).filter(([, value]) => value?.active);
1402
+
1403
+ console.log(chalk.cyan('\nFlowMind Resource Overview'));
1404
+ console.log(chalk.gray('─'.repeat(60)));
1405
+ console.log(chalk.white(`Config dir: ${configDir}`));
1406
+ console.log(chalk.white(`Configured resources: ${configuredResources.length}`));
1407
+ console.log(chalk.white(`Active components: ${activeComponents.length}/${Object.keys(componentStatus || {}).length}`));
1408
+ console.log(chalk.white(`Learned bindings: ${bindings.length}`));
1409
+
1410
+ console.log(chalk.cyan('\nActive Components'));
1411
+ if (activeComponents.length === 0) {
1412
+ console.log(chalk.gray(' (none)'));
1413
+ } else {
1414
+ for (const [name, info] of activeComponents) {
1415
+ const provider = info.provider ? ` provider=${info.provider}` : '';
1416
+ console.log(chalk.white(` - ${name}${provider}`));
1417
+ }
1418
+ }
1419
+
1420
+ console.log(chalk.cyan('\nLearned Bindings'));
1421
+ if (bindings.length === 0) {
1422
+ console.log(chalk.gray(' (none)'));
1423
+ console.log(chalk.gray(' Save one with natural language, for example:'));
1424
+ console.log(chalk.white(' flowmind "绑定零售业务 mcp=yapi-mcp 地址=https://yapi.example.com token=xxx project=retail"'));
1425
+ } else {
1426
+ for (const binding of bindings.slice(0, 8)) {
1427
+ const label = `${binding.business || 'default'} [${binding.componentType || 'unknown'}]`;
1428
+ console.log(chalk.white(` - ${label}`));
1429
+ const details = describeConnection(binding);
1430
+ if (details.length > 0) {
1431
+ console.log(chalk.gray(` ${details.join(' ')}`));
1432
+ }
1433
+ }
1434
+ if (bindings.length > 8) {
1435
+ console.log(chalk.gray(` ... and ${bindings.length - 8} more`));
1436
+ }
1437
+ }
1438
+
1439
+ console.log(chalk.cyan('\nCommon Commands'));
1440
+ for (const example of getResourceSetupExamples()) {
1441
+ console.log(chalk.white(` ${example}`));
1442
+ }
1443
+
1444
+ console.log(chalk.cyan('\nNext Step'));
1445
+ console.log(chalk.white(' For deployment workflows, bind `friday-auto-flow` first, then run your deploy request in natural language.'));
1446
+ }
1447
+
1448
+ function displayResourceSyncSummary(summary) {
1449
+ console.log(chalk.cyan('\nFlowMind Sync Complete'));
1450
+ console.log(chalk.gray('─'.repeat(60)));
1451
+ console.log(chalk.white(`Source: ${summary.sourceDir}`));
1452
+ console.log(chalk.white(`Target: ${summary.targetDir}`));
1453
+ console.log(chalk.white(`Bindings imported: ${summary.counts.importedBindings}`));
1454
+ console.log(chalk.white(`Scenes imported: ${summary.counts.importedScenes}`));
1455
+ console.log(chalk.white(`Components synced: ${summary.counts.components}`));
1456
+ console.log(chalk.gray('\nLocal files updated:'));
1457
+ console.log(chalk.white(` ${summary.files.resourceConfig}`));
1458
+ console.log(chalk.white(` ${summary.files.componentConfig}`));
1459
+ console.log(chalk.white(` ${summary.files.resourceBindings}`));
1460
+ console.log(chalk.white(` ${summary.files.scenes}`));
1461
+ }
1462
+
1340
1463
  function displayAIStatus(status) {
1341
1464
  console.log(chalk.cyan('\n┌─────────────────────────────────────────────────────┐'));
1342
1465
  console.log(chalk.cyan('│ AI Model Status │'));
@@ -1404,10 +1527,19 @@ async function openInEditor(filePath) {
1404
1527
  program
1405
1528
  .command('tui')
1406
1529
  .description('Launch enhanced TUI with split panels, skill browser, and dragon display')
1407
- .action(async () => {
1530
+ .option('--ascii', 'Force ASCII-safe terminal rendering')
1531
+ .action(async (options) => {
1408
1532
  let stdinWrapper = null;
1409
1533
  let stdinForwarder = null;
1410
1534
  try {
1535
+ const managedHost = detectManagedCliHost();
1536
+ if (managedHost) {
1537
+ console.error(chalk.red(`TUI is not supported inside ${managedHost.name}.`));
1538
+ console.log(chalk.yellow(getNestedTuiGuardMessage(managedHost)));
1539
+ process.exitCode = 1;
1540
+ return;
1541
+ }
1542
+
1411
1543
  // Register .jsx extension for CJS
1412
1544
  require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
1413
1545
 
@@ -1415,42 +1547,48 @@ program
1415
1547
  const { render } = require('ink');
1416
1548
  const { PassThrough } = require('stream');
1417
1549
  const App = require('../tui/app.jsx');
1550
+ const asciiMode = options.ascii || shouldUseAsciiUi();
1418
1551
 
1419
1552
  const fm = await initFlowMind();
1420
1553
 
1421
- // Create a stdin wrapper to handle non-TTY environments (e.g., piped stdin).
1422
- // Ink v3's useInput hook calls setRawMode(true) which throws if stdin is not a TTY.
1423
1554
  const realStdin = process.stdin;
1424
- stdinWrapper = new PassThrough();
1425
- stdinWrapper.isTTY = true;
1426
- stdinWrapper.isRaw = false;
1427
- stdinWrapper.setRawMode = (mode) => {
1428
- try {
1429
- if (realStdin.setRawMode) {
1430
- realStdin.setRawMode(mode);
1431
- }
1432
- } catch (e) {
1433
- // Suppress raw mode errors in non-TTY environments
1434
- }
1435
- return stdinWrapper;
1436
- };
1437
- // Forward real stdin data to the wrapper (store reference for cleanup)
1438
- if (realStdin.readable) {
1439
- stdinForwarder = (chunk) => {
1440
- if (!stdinWrapper.destroyed) {
1441
- try {
1442
- stdinWrapper.write(chunk);
1443
- } catch (e) {
1444
- // Ignore write-after-destroy errors
1555
+ let inkStdin = realStdin;
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);
1445
1566
  }
1567
+ } catch (e) {
1568
+ // Suppress raw mode errors in non-TTY environments
1446
1569
  }
1570
+ return stdinWrapper;
1447
1571
  };
1448
- realStdin.on('data', stdinForwarder);
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;
1449
1587
  }
1450
1588
 
1451
1589
  const { unmount, waitUntilExit } = render(
1452
- React.createElement(App, { flowmind: fm }),
1453
- { stdin: stdinWrapper }
1590
+ React.createElement(App, { flowmind: fm, asciiMode: asciiMode }),
1591
+ { stdin: inkStdin }
1454
1592
  );
1455
1593
  await waitUntilExit();
1456
1594
  unmount();
@@ -1475,10 +1613,19 @@ program
1475
1613
  program
1476
1614
  .command('dashboard')
1477
1615
  .description('Launch real-time monitoring dashboard for MCP activity and events')
1478
- .action(async () => {
1616
+ .option('--ascii', 'Force ASCII-safe terminal rendering')
1617
+ .action(async (options) => {
1479
1618
  let stdinWrapper = null;
1480
1619
  let stdinForwarder = null;
1481
1620
  try {
1621
+ const managedHost = detectManagedCliHost();
1622
+ if (managedHost) {
1623
+ console.error(chalk.red(`Dashboard is not supported inside ${managedHost.name}.`));
1624
+ console.log(chalk.yellow(getNestedTuiGuardMessage(managedHost)));
1625
+ process.exitCode = 1;
1626
+ return;
1627
+ }
1628
+
1482
1629
  // Register .jsx extension for CJS
1483
1630
  require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
1484
1631
 
@@ -1487,38 +1634,45 @@ program
1487
1634
  const { PassThrough } = require('stream');
1488
1635
  const DashboardApp = require('../dashboard/app.jsx');
1489
1636
  const eventBus = require('../core/event-bus');
1637
+ const asciiMode = options.ascii || shouldUseAsciiUi();
1490
1638
 
1491
1639
  const fm = await initFlowMind();
1492
1640
 
1493
- // Create a stdin wrapper to handle non-TTY environments
1494
1641
  const realStdin = process.stdin;
1495
- stdinWrapper = new PassThrough();
1496
- stdinWrapper.isTTY = true;
1497
- stdinWrapper.isRaw = false;
1498
- stdinWrapper.setRawMode = (mode) => {
1499
- try {
1500
- if (realStdin.setRawMode) {
1501
- realStdin.setRawMode(mode);
1502
- }
1503
- } catch (e) {
1504
- // Suppress raw mode errors in non-TTY environments
1505
- }
1506
- return stdinWrapper;
1507
- };
1508
- if (realStdin.readable) {
1509
- stdinForwarder = (chunk) => {
1510
- if (!stdinWrapper.destroyed) {
1511
- try {
1512
- stdinWrapper.write(chunk);
1513
- } catch (e) { /* ignore write-after-destroy */ }
1642
+ let inkStdin = realStdin;
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
1514
1656
  }
1657
+ return stdinWrapper;
1515
1658
  };
1516
- realStdin.on('data', stdinForwarder);
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;
1517
1671
  }
1518
1672
 
1519
1673
  const { unmount, waitUntilExit } = render(
1520
- React.createElement(DashboardApp, { flowmind: fm, eventBus }),
1521
- { stdin: stdinWrapper }
1674
+ React.createElement(DashboardApp, { flowmind: fm, eventBus, asciiMode: asciiMode }),
1675
+ { stdin: inkStdin }
1522
1676
  );
1523
1677
  await waitUntilExit();
1524
1678
  unmount();
@@ -1660,6 +1814,16 @@ program
1660
1814
  });
1661
1815
 
1662
1816
  // Parse arguments
1817
+ const knownCommands = new Set([
1818
+ 'init', 'process', 'p', 'learn', 'scenes', 'skills', 'skill', 'resource', 'res',
1819
+ 'stats', 'honor', 'config', 'ai', 'tui', 'dashboard', 'doctor', 'update', 'help'
1820
+ ]);
1821
+ const cliArgs = process.argv.slice(2);
1822
+
1823
+ if (cliArgs.length > 0 && !cliArgs[0].startsWith('-') && !knownCommands.has(cliArgs[0])) {
1824
+ process.argv.splice(2, 0, 'process');
1825
+ }
1826
+
1663
1827
  program.parse(process.argv);
1664
1828
 
1665
1829
  // Default to interactive mode if no command specified
@@ -38,6 +38,15 @@ class WorkflowAdapter extends McpAdapter {
38
38
  throw new Error('Subclasses must implement startPipelineRun()');
39
39
  }
40
40
 
41
+ /**
42
+ * Start a batch pipeline run.
43
+ * @param {object} params
44
+ * @returns {Promise<object>}
45
+ */
46
+ async startBatchPipelineRun(params) {
47
+ throw new Error('Subclasses must implement startBatchPipelineRun()');
48
+ }
49
+
41
50
  /**
42
51
  * Get pipeline run status.
43
52
  * @param {string} pipelineId
@@ -19,6 +19,8 @@ class ConfigManager {
19
19
  * Load configuration
20
20
  */
21
21
  async load() {
22
+ this.config = this.deepMerge({}, this.defaults);
23
+
22
24
  // Try to load from specified path
23
25
  if (this.configPath) {
24
26
  await this.loadFromFile(this.configPath);
@@ -181,7 +183,12 @@ class ConfigManager {
181
183
  autoApply: true,
182
184
  confidenceThreshold: 0.7,
183
185
  maxRecordsPerSkill: 100,
184
- storagePath: path.join(this.getHomeDir(), '.flowmind', 'learning')
186
+ storagePath: path.join(this.getHomeDir(), '.flowmind', 'learning'),
187
+ autoPromote: {
188
+ enabled: true,
189
+ resourceBindingMinUses: 3,
190
+ sceneMinUses: 2
191
+ }
185
192
  },
186
193
 
187
194
  // Scene mapping settings
package/core/index.js CHANGED
@@ -190,6 +190,7 @@ class FlowMind {
190
190
 
191
191
  // Get learning rules for this skill
192
192
  const learnings = await this.learning.getSkillLearnings(skill.name);
193
+ const resourceBinding = await this.learning.matchResourceBinding(input, skill.name);
193
194
 
194
195
  // Apply learning rules to context
195
196
  const enhancedContext = {
@@ -197,12 +198,21 @@ class FlowMind {
197
198
  flowmind: context.flowmind || this,
198
199
  currentSkill: context.currentSkill || skill.name,
199
200
  learnings: learnings,
200
- preferences: await this.learning.getPreferences(skill.name)
201
+ preferences: await this.learning.getPreferences(skill.name),
202
+ resourceBinding
201
203
  };
202
204
 
203
205
  // Execute skill
204
206
  const result = await skill.execute(input, enhancedContext);
205
207
 
208
+ if (resourceBinding?.id) {
209
+ await this.learning.markResourceBindingUsed(resourceBinding.id, {
210
+ input,
211
+ skillName: skill.name,
212
+ currentSkill: enhancedContext.currentSkill
213
+ });
214
+ }
215
+
206
216
  const duration = Date.now() - skillStartTime;
207
217
 
208
218
  // Emit skill execution event
@@ -226,23 +236,29 @@ class FlowMind {
226
236
  const { scene, params } = sceneMatch;
227
237
  const results = [];
228
238
 
229
- for (const step of scene.workflow.skills) {
230
- const skill = this.skills.get(step.skill);
231
- if (!skill) continue;
232
-
233
- const stepContext = {
234
- ...context,
235
- params: { ...step.params, ...params },
236
- previousResults: results,
237
- flowmind: this,
238
- currentSkill: step.skill
239
- };
239
+ try {
240
+ for (const step of scene.workflow.skills) {
241
+ const skill = this.skills.get(step.skill);
242
+ if (!skill) continue;
243
+
244
+ const stepContext = {
245
+ ...context,
246
+ params: { ...step.params, ...params },
247
+ previousResults: results,
248
+ flowmind: this,
249
+ currentSkill: step.skill
250
+ };
240
251
 
241
- const result = await this.executeWithLearning(skill, input, stepContext);
242
- results.push({
243
- skill: step.skill,
244
- result: result
245
- });
252
+ const result = await this.executeWithLearning(skill, input, stepContext);
253
+ results.push({
254
+ skill: step.skill,
255
+ result: result
256
+ });
257
+ }
258
+ await this.matcher.updateSceneStats(scene.id, true, { input });
259
+ } catch (error) {
260
+ await this.matcher.updateSceneStats(scene.id, false, { input });
261
+ throw error;
246
262
  }
247
263
 
248
264
  return {