story-weaver-ai
Version:
A narrative development system for AI-driven storytelling with Jungian psychology
182 lines (160 loc) • 5.83 kB
JavaScript
/**
* Story Weaver MCP Server - Refine Story Tool
*
* @author Sean Pavlak
* @github https://github.com/seanpavlak/cursor-story-master
*
* Tool for refining stories based on analysis and Jungian psychological principles.
*/
import fs from 'fs/promises';
import path from 'path';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import logger from '../logger.js';
// Get the directory path for the module
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '../../..');
/**
* Refine a story based on analysis and Jungian psychological principles
*/
export const refineStoryTool = {
name: 'refine_story',
description: 'Refine a story based on analysis, focusing on structure, character development, themes, and psychological depth.',
parameters: {
type: 'object',
properties: {
input_file: {
type: 'string',
description: 'Path to the story file to refine'
},
analysis_file: {
type: 'string',
description: 'Path to the analysis file to use for refinement'
},
output_file: {
type: 'string',
description: 'Path to save the refined story (default: refined_story.md)'
},
focus_areas: {
type: 'array',
items: {
type: 'string'
},
description: 'Areas to focus on (e.g., "pacing", "characters", "themes")'
},
tone_adjustment: {
type: 'string',
description: 'Desired tone adjustment (e.g., "darker", "lighter", "more hopeful")'
},
guidance: {
type: 'string',
description: 'Additional refinement guidance or specific instructions'
}
},
required: ['input_file', 'analysis_file']
},
async handler(params) {
try {
logger.info(`Refining story file: ${params.input_file} with analysis: ${params.analysis_file}`);
// Default output file if not specified
const outputFile = params.output_file || 'refined_story.md';
// Set up command arguments
const args = [
path.join(rootDir, 'bin/story-weaver.js'),
'refine-story',
'-i', params.input_file,
'-a', params.analysis_file,
'-o', outputFile
];
// Add focus areas if specified
if (params.focus_areas && params.focus_areas.length > 0) {
args.push('-f', ...params.focus_areas);
}
// Add tone adjustment if specified
if (params.tone_adjustment) {
args.push('-t', params.tone_adjustment);
}
// Add additional guidance if specified
if (params.guidance) {
args.push('-g', params.guidance);
}
// Execute the command
logger.debug(`Executing command: ${args.join(' ')}`);
const result = spawnSync('node', args, {
encoding: 'utf8',
cwd: process.cwd()
});
if (result.error) {
throw new Error(`Failed to execute: ${result.error.message}`);
}
if (result.status !== 0) {
throw new Error(`Command failed with status ${result.status}: ${result.stderr}`);
}
// Check if the refined story file was created
await fs.access(outputFile);
// Get file statistics to determine size changes
const originalStats = await fs.stat(params.input_file);
const refinedStats = await fs.stat(outputFile);
// Calculate changes as percentages
const sizeDifference = refinedStats.size - originalStats.size;
const percentChange = ((sizeDifference / originalStats.size) * 100).toFixed(2);
// Read a sample of the refined story for preview
const previewContent = await readSample(outputFile, 500);
// Return the refinement results
return {
status: 'success',
message: `Story refinement complete. Results saved to ${outputFile}`,
file_path: outputFile,
changes: {
original_size: originalStats.size,
refined_size: refinedStats.size,
difference: sizeDifference,
percent_change: `${percentChange}%`,
increased: sizeDifference > 0,
decreased: sizeDifference < 0
},
focus_areas: params.focus_areas || [],
tone_adjustment: params.tone_adjustment || null,
preview: previewContent
};
} catch (error) {
logger.error(`Story refinement failed: ${error.message}`);
return {
status: 'error',
message: `Failed to refine story: ${error.message}`
};
}
}
};
/**
* Read a sample of the content from a file
* @param {string} filePath - Path to the file
* @param {number} maxChars - Maximum number of characters to read
* @returns {string} Sample content
*/
async function readSample(filePath, maxChars = 500) {
try {
const handle = await fs.open(filePath, 'r');
const buffer = Buffer.alloc(maxChars);
const { bytesRead } = await handle.read(buffer, 0, maxChars, 0);
await handle.close();
let content = buffer.toString('utf8', 0, bytesRead);
// If we didn't read the entire file, add ellipsis
if (bytesRead === maxChars) {
// Try to end at a sentence or word boundary
const lastPeriod = content.lastIndexOf('.');
const lastSpace = content.lastIndexOf(' ');
let endIndex = Math.max(lastPeriod, lastSpace);
if (endIndex < content.length - 20) { // Ensure we're not cutting off too early
content = content.substring(0, endIndex + 1) + '...';
} else {
content = content + '...';
}
}
return content;
} catch (error) {
logger.error(`Failed to read sample: ${error.message}`);
return '[Preview unavailable]';
}
}