feat(live-values): --live-values CLI flag + server pump wiring
Task 7 of v0.3 plan.
ServerConfig.liveValues?: boolean.
bin.ts: --live-values commander option, plumbs into config, banner
'Live values: ENABLED (poll 500ms; writes tui-live-values.json)'.
server.ts: when config.liveValues is on, instantiate LiveValuesPump
with deps:
- readSelection from state-read
- readPouFile = fs.promises.readFile
- readVariable = run the existing read_variable script via the
executor; parse 'Value: <v>' from stdout
- writeLiveValues from live-values-write
Lifetime: start() right after server.connect(); stop() in the
shutdown handler before launcher.shutdown.
defaultStateDir() helper extracted; defaultStateFilePath() and the
new defaultLiveValuesFilePath() share it.
When the flag is off (default), the pump is never instantiated and
the server runs unchanged.
This commit is contained in:
parent
c81ce33a4b
commit
8aa2553996
3 changed files with 66 additions and 4 deletions
|
|
@ -48,6 +48,7 @@ program
|
|||
.option('--keep-alive', 'Keep CODESYS running after server stops', false)
|
||||
.option('--auto-mirror', 'Re-run mirror_export after every modifying tool so an external editor watching <projectDir>/mcp-mirror/ sees changes live', false)
|
||||
.option('--approve-edits', 'Gate modifying MCP tools behind a phobiCS-tui y/n diff prompt', false)
|
||||
.option('--live-values', 'Pump runtime values for the selected POU into tui-live-values.json so phobiCS-tui can overlay them inline (500ms poll). Requires the runtime to be online; failures are silent.', false)
|
||||
.option('--timeout <ms>', 'Default command timeout in ms', '60000')
|
||||
.option('--verbose', 'Enable verbose logging')
|
||||
.option('--debug', 'Enable debug logging (more verbose)')
|
||||
|
|
@ -199,6 +200,7 @@ if (opts.sshVersion) {
|
|||
mode: (opts.mode === 'headless' ? 'headless' : 'persistent') as ExecutionMode,
|
||||
autoMirror: opts.autoMirror || false,
|
||||
approveEdits: opts.approveEdits || false,
|
||||
liveValues: opts.liveValues || false,
|
||||
};
|
||||
|
||||
process.stderr.write(`Starting CODESYS MCP Server v${version}\n`);
|
||||
|
|
@ -212,6 +214,9 @@ if (opts.sshVersion) {
|
|||
if (config.approveEdits) {
|
||||
process.stderr.write(` Approve edits: ENABLED (modifying tools will prompt via phobiCS-tui)\n`);
|
||||
}
|
||||
if (config.liveValues) {
|
||||
process.stderr.write(` Live values: ENABLED (poll 500ms; writes tui-live-values.json)\n`);
|
||||
}
|
||||
|
||||
startMcpServer(config).catch((err) => {
|
||||
process.stderr.write(`FATAL: ${err.message}\n`);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import { inspectProjectFile } from './inspect';
|
|||
import { parseProfileName } from './detect';
|
||||
import { decideOpenProjectPreflight } from './preflight';
|
||||
import { readSelection } from './state-read';
|
||||
import { writeLiveValues } from './live-values-write';
|
||||
import { LiveValuesPump } from './live-values-pump';
|
||||
import { runApproveGate, gateOpForTool } from './approve-gate';
|
||||
|
||||
/**
|
||||
|
|
@ -707,17 +709,25 @@ export async function buildGetUserSelectionResponse(stateFilePath: string) {
|
|||
};
|
||||
}
|
||||
|
||||
function defaultStateFilePath(): string {
|
||||
function defaultStateDir(): string {
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA;
|
||||
if (!localAppData) {
|
||||
return path.join(os.homedir(), 'AppData', 'Local', 'codesys-mcp', 'tui-state.json');
|
||||
return path.join(os.homedir(), 'AppData', 'Local', 'codesys-mcp');
|
||||
}
|
||||
return path.join(localAppData, 'codesys-mcp', 'tui-state.json');
|
||||
return path.join(localAppData, 'codesys-mcp');
|
||||
}
|
||||
const xdg = process.env.XDG_STATE_HOME;
|
||||
const base = xdg ?? path.join(os.homedir(), '.local', 'state');
|
||||
return path.join(base, 'codesys-mcp', 'tui-state.json');
|
||||
return path.join(base, 'codesys-mcp');
|
||||
}
|
||||
|
||||
function defaultStateFilePath(): string {
|
||||
return path.join(defaultStateDir(), 'tui-state.json');
|
||||
}
|
||||
|
||||
function defaultLiveValuesFilePath(): string {
|
||||
return path.join(defaultStateDir(), 'tui-live-values.json');
|
||||
}
|
||||
|
||||
export async function startMcpServer(config: ServerConfig): Promise<void> {
|
||||
|
|
@ -728,6 +738,7 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
serverLog.info(`Starting CODESYS Persistent MCP Server v0.1.0`);
|
||||
serverLog.info(`Mode: ${config.mode}`);
|
||||
serverLog.info(`Approve edits: ${config.approveEdits ? 'ON' : 'off'}`);
|
||||
serverLog.info(`Live values: ${config.liveValues ? 'ON' : 'off'}`);
|
||||
serverLog.info(`CODESYS Path: ${config.codesysPath}`);
|
||||
serverLog.info(`Profile: ${config.profileName}`);
|
||||
serverLog.info(`Workspace: ${config.workspaceDir}`);
|
||||
|
|
@ -3140,10 +3151,49 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
server.connect(transport);
|
||||
serverLog.info('MCP Server connected and listening.');
|
||||
|
||||
// ─── Live values pump (opt-in) ───────────────────────────────────────
|
||||
|
||||
let liveValuesPump: LiveValuesPump | null = null;
|
||||
if (config.liveValues) {
|
||||
liveValuesPump = new LiveValuesPump(
|
||||
{
|
||||
stateFilePath: defaultStateFilePath(),
|
||||
liveValuesFilePath: defaultLiveValuesFilePath(),
|
||||
intervalMs: 500,
|
||||
},
|
||||
{
|
||||
readSelection,
|
||||
readPouFile: (absPath) => fs.promises.readFile(absPath, 'utf8'),
|
||||
readVariable: async (projectFilePath, variablePath) => {
|
||||
// Reuse the existing read_variable script. Returns the value as
|
||||
// a string captured from script stdout. Errors throw.
|
||||
const script = scriptManager.prepareScriptWithHelpers(
|
||||
'read_variable',
|
||||
{ PROJECT_FILE_PATH: projectFilePath, VARIABLE_PATH: variablePath },
|
||||
['ensure_project_open', 'ensure_online_connection']
|
||||
);
|
||||
const result = await executor.executeScript(script);
|
||||
if (!result.success || !result.output.includes('SCRIPT_SUCCESS')) {
|
||||
throw new Error(result.error || 'read_variable failed');
|
||||
}
|
||||
// The script prints `Value: <value>` on the success line.
|
||||
const m = /Value:\s*(.*)$/m.exec(result.output);
|
||||
return m ? m[1].trim() : '';
|
||||
},
|
||||
writeLiveValues,
|
||||
}
|
||||
);
|
||||
liveValuesPump.start();
|
||||
serverLog.info('Live-values pump started (500ms)');
|
||||
}
|
||||
|
||||
// ─── Graceful Shutdown ───────────────────────────────────────────────
|
||||
|
||||
const shutdown = async () => {
|
||||
serverLog.info('Shutdown signal received');
|
||||
if (liveValuesPump) {
|
||||
liveValuesPump.stop();
|
||||
}
|
||||
if (launcher) {
|
||||
try {
|
||||
await launcher.shutdown();
|
||||
|
|
|
|||
|
|
@ -73,6 +73,13 @@ export interface ServerConfig extends LauncherConfig {
|
|||
* so existing scripted flows are not regressed.
|
||||
*/
|
||||
approveEdits?: boolean;
|
||||
/**
|
||||
* If true, run a background pump that reads runtime values for the
|
||||
* variables of the user's currently-selected POU and writes them to
|
||||
* tui-live-values.json so the TUI Viewer can overlay them inline.
|
||||
* Off by default. 500 ms interval (fixed in v0.3).
|
||||
*/
|
||||
liveValues?: boolean;
|
||||
}
|
||||
|
||||
/** Script template parameters */
|
||||
|
|
|
|||
Loading…
Reference in a new issue