coaian
Version:
Creative Orientation AI Agentic Memories - Narrative Beat Extension with IAIP relational integration
1,070 lines (996 loc) ⢠102 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { promises as fs } from 'fs';
import path from 'path';
import { LLM_GUIDANCE } from "./generated-llm-guidance.js";
import { fileURLToPath } from 'url';
import minimist from 'minimist';
import { isAbsolute } from 'path';
import { validate, ValidationSchemas } from './validation.js';
// Parse args and handle paths safely
const argv = minimist(process.argv.slice(2));
// Handle help command
if (argv.help || argv.h) {
console.log(`
š§ COAIA Memory - Creative-Oriented AI Assistant Memory System v2.1.0
Based on Robert Fritz's Structural Tension methodology
DESCRIPTION:
MCP server that extends knowledge graphs with structural tension charts for
creative-oriented memory management. Supports advancing patterns, telescoping
charts, and natural language interaction for AI assistants.
USAGE:
coaia-memory [OPTIONS]
npx coaia-memory [OPTIONS]
OPTIONS:
--memory-path PATH Custom path for memory storage (default: ./memory.jsonl)
--help, -h Show this help message
ENVIRONMENT VARIABLES:
COAIA_TOOLS Comma or space separated list of tool groups and/or individual tools to enable
(default: "STC_TOOLS,init_llm_guidance")
COAIA_DISABLED_TOOLS Comma or space separated list of tools to disable
(useful for selectively removing tools from a group)
TOOL GROUPS:
STC_TOOLS All structural tension chart tools (11 tools) - recommended for creative work
KG_TOOLS All knowledge graph tools (9 tools) - for traditional entity/relation work
CORE_TOOLS Essential tools only (4 tools) - minimal viable set
EXAMPLES:
# Use only STC tools (default)
coaia-memory --memory-path ./memory.jsonl
# Enable both STC and KG tools
COAIA_TOOLS="STC_TOOLS KG_TOOLS" coaia-memory --memory-path ./memory.jsonl
# Use only core tools
COAIA_TOOLS="CORE_TOOLS" coaia-memory --memory-path ./memory.jsonl
# Enable STC tools but disable specific tools
COAIA_TOOLS="STC_TOOLS" COAIA_DISABLED_TOOLS="delete_entities,delete_relations" coaia-memory
# Enable specific individual tools
COAIA_TOOLS="create_structural_tension_chart add_action_step list_active_charts" coaia-memory
CORE FEATURES:
š Structural Tension Charts
⢠Create charts with desired outcomes, current reality, and action steps
⢠Automatic due date distribution for strategic timing
⢠Progress tracking and completion monitoring
š Telescoping Support
⢠Break down action steps into detailed sub-charts
⢠Proper due date inheritance from parent steps
⢠Navigate between overview and details seamlessly
š Advancing Patterns
⢠Completed actions flow into current reality automatically
⢠Success builds momentum for continued advancement
⢠Prevents oscillating patterns through structural awareness
MCP TOOLS AVAILABLE:
Chart Management (Common Workflow):
⢠list_active_charts - START HERE: See all charts and their progress
⢠add_action_step - Add strategic actions to existing charts
⢠telescope_action_step - Break down action steps into detailed sub-charts
⢠update_action_progress - Track progress without completing actions
⢠mark_action_complete - Complete actions & update reality
⢠update_current_reality - Add observations directly to current reality
⢠create_structural_tension_chart - Create new chart with outcome & reality
Chart Analysis (Advanced):
⢠get_chart_progress - Detailed progress (redundant after list_active_charts)
⢠open_nodes - Inspect specific chart components by exact name
⢠read_graph - Dump all data (rarely needed)
Knowledge Graph (Traditional):
⢠create_entities - Add entities (people, concepts, events)
⢠create_relations - Connect entities with relationships
⢠add_observations - Record information about entities
⢠search_nodes - Search across all stored information
⢠read_graph - Export complete graph structure
EXAMPLE USAGE:
# Start with custom memory path
coaia-memory --memory-path /path/to/my-charts.jsonl
# Use in Claude Desktop (add to claude_desktop_config.json):
{
"mcpServers": {
"coaia-memory": {
"command": "npx",
"args": ["-y", "coaia-memory", "--memory-path", "./charts.jsonl"]
}
}
}
NATURAL LANGUAGE PATTERNS:
Creating Charts:
"I want to create a mobile app in 3 months"
"My desired outcome is to establish a morning routine"
Progress Tracking:
"I completed the research phase yesterday"
"Show me progress on my Python learning goal"
Telescoping:
"Break down the Django tutorial step further"
"I need more detail on the deployment action"
CREATIVE ORIENTATION PRINCIPLES:
ā
Focus on Creation (not problem-solving):
⢠"I want to create..." vs "I need to fix..."
⢠"My desired outcome..." vs "The problem is..."
ā
Structural Tension Awareness:
⢠Always pair desired outcomes with current reality
⢠Honest assessment creates productive tension
⢠Action steps are strategic secondary action we choose todo to achive the primary goal
ā
Advancing Patterns:
⢠Success builds on success
⢠Completed actions become part of current reality
⢠Momentum creates natural progression toward goals
PHILOSOPHY:
COAIA Memory recognizes that structure determines behavior. By organizing
memory around structural tension rather than problem-solving patterns, it
naturally forms a structure that advances and helps build, not just the life you want, but the technologies to supports it's manifestation (hopefully!).
CREDITS:
⢠Author: J.Guillaume D.-Isabelle <jgi@jgwill.com>
⢠Methodology: Robert Fritz - https://robertfritz.com
⢠Foundation: Shane Holloman (original mcp-knowledge-graph)
⢠License: MIT
For more information, see: CLAUDE.md in the package directory
`);
process.exit(0);
}
let memoryPath = argv['memory-path'];
// If a custom path is provided, ensure it's absolute
if (memoryPath && !isAbsolute(memoryPath)) {
memoryPath = path.resolve(process.cwd(), memoryPath);
}
// Tool filtering configuration
const TOOL_GROUPS = {
STC_TOOLS: [
'create_structural_tension_chart',
'telescope_action_step',
'add_action_step',
'remove_action_step',
'mark_action_complete',
'get_chart_progress',
'list_active_charts',
'update_action_progress',
'update_current_reality',
'update_desired_outcome',
'creator_moment_of_truth'
],
NARRATIVE_TOOLS: [
'create_narrative_beat',
'telescope_narrative_beat',
'list_narrative_beats'
],
KG_TOOLS: [
'create_entities',
'create_relations',
'add_observations',
'delete_entities',
'delete_observations',
'delete_relations',
'search_nodes',
'open_nodes',
'read_graph'
],
CORE_TOOLS: [
'list_active_charts',
'create_structural_tension_chart',
'add_action_step',
'mark_action_complete'
]
};
function getEnabledTools() {
const enabledTools = new Set();
// Check for COAIA_DISABLED_TOOLS env var (comma or space separated)
const disabledStr = process.env.COAIA_DISABLED_TOOLS || '';
const disabledTools = new Set(disabledStr.split(/[,\s]+/).filter(t => t.trim()));
// Determine which tools to enable
const enabledGroupsStr = process.env.COAIA_TOOLS || 'STC_TOOLS,NARRATIVE_TOOLS,init_llm_guidance';
const enabledGroups = enabledGroupsStr.split(/[,\s]+/).filter(t => t.trim());
enabledGroups.forEach(group => {
const groupTools = TOOL_GROUPS[group];
if (groupTools) {
groupTools.forEach(tool => enabledTools.add(tool));
}
else {
// Assume it's an individual tool name
enabledTools.add(group);
}
});
// Remove disabled tools
disabledTools.forEach(tool => enabledTools.delete(tool));
return enabledTools;
}
// Define the path to the JSONL file
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Use the custom path or default to the installation directory
const MEMORY_FILE_PATH = memoryPath || path.join(__dirname, 'memory.jsonl');
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager {
async loadGraph() {
try {
const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8");
const lines = data.split("\n").filter(line => line.trim() !== "");
return lines.reduce((graph, line) => {
const item = JSON.parse(line);
if (item.type === "entity")
graph.entities.push(item);
if (item.type === "relation")
graph.relations.push(item);
// Support narrative_beat entities (convert to entity format)
if (item.type === "narrative_beat") {
const narrativeBeat = {
name: item.name,
entityType: 'narrative_beat',
observations: item.observations || [],
metadata: {
...item.metadata,
narrative: item.narrative,
relationalAlignment: item.relational_alignment,
fourDirections: item.four_directions
}
};
graph.entities.push(narrativeBeat);
}
return graph;
}, { entities: [], relations: [] });
}
catch (error) {
if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
return { entities: [], relations: [] };
}
throw error;
}
}
// Helper function to extract current reality from user context
// Maintains structural tension by requiring explicit assessment
extractCurrentRealityFromContext(userInput, actionStepTitle) {
// Common patterns that indicate current reality assessment
const realityPatterns = [
/(?:currently|right now|at present|today)\s+(.{10,})/i,
/(?:i am|we are|the situation is)\s+(.{10,})/i,
/(?:i have|we have|there is|there are)\s+(.{10,})/i,
/(?:my current|our current|the current)\s+(.{10,})/i
];
for (const pattern of realityPatterns) {
const match = userInput.match(pattern);
if (match && match[1]) {
return match[1].trim();
}
}
// If no explicit current reality found, require assessment
return null;
}
async saveGraph(graph) {
const lines = [
...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
];
await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n"));
}
async createEntities(entities) {
const graph = await this.loadGraph();
const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
graph.entities.push(...newEntities);
await this.saveGraph(graph);
return newEntities;
}
async createRelations(relations) {
const graph = await this.loadGraph();
const newRelations = relations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
existingRelation.to === r.to &&
existingRelation.relationType === r.relationType));
graph.relations.push(...newRelations);
await this.saveGraph(graph);
return newRelations;
}
async addObservations(observations) {
const graph = await this.loadGraph();
const results = observations.map(o => {
const entity = graph.entities.find(e => e.name === o.entityName);
if (!entity) {
throw new Error(`Entity with name ${o.entityName} not found`);
}
const newObservations = o.contents.filter(content => !entity.observations.includes(content));
entity.observations.push(...newObservations);
return { entityName: o.entityName, addedObservations: newObservations };
});
await this.saveGraph(graph);
return results;
}
async deleteEntities(entityNames) {
const graph = await this.loadGraph();
graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
await this.saveGraph(graph);
}
async deleteObservations(deletions) {
const graph = await this.loadGraph();
deletions.forEach(d => {
const entity = graph.entities.find(e => e.name === d.entityName);
if (entity) {
entity.observations = entity.observations.filter(o => !d.observations.includes(o));
}
});
await this.saveGraph(graph);
}
async deleteRelations(relations) {
const graph = await this.loadGraph();
graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
r.to === delRelation.to &&
r.relationType === delRelation.relationType));
await this.saveGraph(graph);
}
async readGraph() {
return this.loadGraph();
}
// Very basic search function
async searchNodes(query) {
const graph = await this.loadGraph();
// Filter entities
const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())));
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
const filteredGraph = {
entities: filteredEntities,
relations: filteredRelations,
};
return filteredGraph;
}
async openNodes(names) {
const graph = await this.loadGraph();
// Filter entities
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
const filteredGraph = {
entities: filteredEntities,
relations: filteredRelations,
};
return filteredGraph;
}
// COAIA-specific methods for structural tension charts and creative processes
async createStructuralTensionChart(desiredOutcome, currentReality, dueDate, actionSteps) {
// Educational validation for creative orientation
const problemSolvingWords = ['fix', 'solve', 'eliminate', 'prevent', 'stop', 'avoid', 'reduce', 'remove'];
const detectedProblemWords = problemSolvingWords.filter(word => desiredOutcome.toLowerCase().includes(word));
if (detectedProblemWords.length > 0) {
throw new Error(`š CREATIVE ORIENTATION REQUIRED
Desired Outcome: "${desiredOutcome}"
ā **Problem**: Contains problem-solving language: "${detectedProblemWords.join(', ')}"
š **Principle**: Structural Tension Charts use creative orientation - focus on what you want to CREATE, not what you want to eliminate.
šÆ **Reframe Your Outcome**:
Instead of elimination ā Creation focus
ā
**Examples**:
- Instead of: "Fix communication problems"
- Use: "Establish clear, effective communication practices"
- Instead of: "Reduce website loading time"
- Use: "Achieve fast, responsive website performance"
**Why This Matters**: Problem-solving creates oscillating patterns. Creative orientation creates advancing patterns toward desired outcomes.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
// Educational validation for current reality
const readinessWords = ['ready to', 'prepared to', 'all set', 'ready for', 'set to'];
const detectedReadinessWords = readinessWords.filter(phrase => currentReality.toLowerCase().includes(phrase));
if (detectedReadinessWords.length > 0) {
throw new Error(`š DELAYED RESOLUTION PRINCIPLE VIOLATION
Current Reality: "${currentReality}"
ā **Problem**: Contains readiness assumptions: "${detectedReadinessWords.join(', ')}"
š **Principle**: "Tolerate discrepancy, tension, and delayed resolution" - Robert Fritz
šÆ **What's Needed**: Factual assessment of your actual current state (not readiness or preparation).
ā
**Examples**:
- Instead of: "Ready to learn Python"
- Use: "Never programmed before, interested in web development"
- Instead of: "Prepared to start the project"
- Use: "Have project requirements, no code written yet"
**Why This Matters**: Readiness assumptions prematurely resolve the structural tension needed for creative advancement.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
const chartId = `chart_${Date.now()}`;
const timestamp = new Date().toISOString();
// Create chart, desired outcome, and current reality entities
const entities = [
{
name: `${chartId}_chart`,
entityType: 'structural_tension_chart',
observations: [`Chart created on ${timestamp}`],
metadata: {
chartId,
dueDate,
level: 0,
createdAt: timestamp,
updatedAt: timestamp
}
},
{
name: `${chartId}_desired_outcome`,
entityType: 'desired_outcome',
observations: [desiredOutcome],
metadata: {
chartId,
dueDate,
createdAt: timestamp,
updatedAt: timestamp
}
},
{
name: `${chartId}_current_reality`,
entityType: 'current_reality',
observations: [currentReality],
metadata: {
chartId,
createdAt: timestamp,
updatedAt: timestamp
}
}
];
// Add action steps if provided
if (actionSteps && actionSteps.length > 0) {
const stepDueDates = this.distributeActionStepDates(new Date(), new Date(dueDate), actionSteps.length);
actionSteps.forEach((step, index) => {
entities.push({
name: `${chartId}_action_${index + 1}`,
entityType: 'action_step',
observations: [step],
metadata: {
chartId,
dueDate: stepDueDates[index].toISOString(),
completionStatus: false,
createdAt: timestamp,
updatedAt: timestamp
}
});
});
}
// Create relations
const relations = [
{
from: `${chartId}_chart`,
to: `${chartId}_desired_outcome`,
relationType: 'contains',
metadata: { createdAt: timestamp }
},
{
from: `${chartId}_chart`,
to: `${chartId}_current_reality`,
relationType: 'contains',
metadata: { createdAt: timestamp }
},
{
from: `${chartId}_current_reality`,
to: `${chartId}_desired_outcome`,
relationType: 'creates_tension_with',
metadata: { createdAt: timestamp }
}
];
// Add action step relations
if (actionSteps && actionSteps.length > 0) {
actionSteps.forEach((_, index) => {
const actionName = `${chartId}_action_${index + 1}`;
relations.push({
from: `${chartId}_chart`,
to: actionName,
relationType: 'contains',
metadata: { createdAt: timestamp }
}, {
from: actionName,
to: `${chartId}_desired_outcome`,
relationType: 'advances_toward',
metadata: { createdAt: timestamp }
});
});
}
// Save to graph
await this.createEntities(entities);
await this.createRelations(relations);
return { chartId, entities, relations };
}
async telescopeActionStep(actionStepName, newCurrentReality, initialActionSteps) {
const graph = await this.loadGraph();
const actionStep = graph.entities.find(e => e.name === actionStepName && e.entityType === 'action_step');
if (!actionStep || !actionStep.metadata?.chartId) {
throw new Error(`Action step ${actionStepName} not found or not properly configured`);
}
const parentChartId = actionStep.metadata.chartId;
const inheritedDueDate = actionStep.metadata.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
const desiredOutcome = actionStep.observations[0]; // Use the action step description as the new desired outcome
const result = await this.createStructuralTensionChart(desiredOutcome, newCurrentReality, inheritedDueDate, initialActionSteps);
// Update the new chart's metadata to reflect telescoping relationship
const newChart = await this.loadGraph();
const chartEntity = newChart.entities.find(e => e.name === `${result.chartId}_chart`);
if (chartEntity && chartEntity.metadata) {
chartEntity.metadata.parentChart = parentChartId;
chartEntity.metadata.parentActionStep = actionStepName;
chartEntity.metadata.level = (actionStep.metadata.level || 0) + 1;
chartEntity.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraph(newChart);
return { chartId: result.chartId, parentChart: parentChartId };
}
async markActionStepComplete(actionStepName) {
const graph = await this.loadGraph();
// An "action step" can be a 'desired_outcome' of a sub-chart, or a simple 'action_step' entity.
const actionStep = graph.entities.find(e => e.name === actionStepName && (e.entityType === 'action_step' || e.entityType === 'desired_outcome'));
if (!actionStep) {
throw new Error(`Action step ${actionStepName} not found`);
}
const chartId = actionStep.metadata?.chartId;
if (!chartId) {
throw new Error(`Chart ID not found for action step ${actionStepName}`);
}
// Mark the action step itself as complete
if (actionStep.metadata) {
actionStep.metadata.completionStatus = true;
actionStep.metadata.updatedAt = new Date().toISOString();
}
// Also mark the parent chart entity as complete
const chartEntity = graph.entities.find(e => e.name === `${chartId}_chart`);
if (chartEntity && chartEntity.metadata) {
chartEntity.metadata.completionStatus = true;
chartEntity.metadata.updatedAt = new Date().toISOString();
}
// Structural tension principle: completed action steps flow into the CURRENT REALITY
// of the PARENT chart, advancing the overall structure.
const parentChartId = chartEntity?.metadata?.parentChart;
if (parentChartId) {
const parentCurrentReality = graph.entities.find(e => e.name === `${parentChartId}_current_reality` &&
e.entityType === 'current_reality');
if (parentCurrentReality) {
const completionMessage = `Completed: ${actionStep.observations[0]}`;
if (!parentCurrentReality.observations.includes(completionMessage)) {
parentCurrentReality.observations.push(completionMessage);
if (parentCurrentReality.metadata) {
parentCurrentReality.metadata.updatedAt = new Date().toISOString();
}
}
}
}
await this.saveGraph(graph);
}
async getChartProgress(chartId) {
const graph = await this.loadGraph();
const actionSteps = graph.entities.filter(e => e.entityType === 'action_step' &&
e.metadata?.chartId === chartId);
const completedActions = actionSteps.filter(e => e.metadata?.completionStatus === true).length;
const totalActions = actionSteps.length;
const progress = totalActions > 0 ? completedActions / totalActions : 0;
// Find next incomplete action step with earliest due date
const incompleteActions = actionSteps
.filter(e => e.metadata?.completionStatus !== true)
.sort((a, b) => {
const dateA = new Date(a.metadata?.dueDate || '').getTime();
const dateB = new Date(b.metadata?.dueDate || '').getTime();
return dateA - dateB;
});
const chart = graph.entities.find(e => e.name === `${chartId}_chart`);
return {
chartId,
progress,
completedActions,
totalActions,
nextAction: incompleteActions[0]?.name,
dueDate: chart?.metadata?.dueDate
};
}
distributeActionStepDates(startDate, endDate, stepCount) {
const totalTime = endDate.getTime() - startDate.getTime();
const stepInterval = totalTime / (stepCount + 1); // +1 to leave space before final due date
const dates = [];
for (let i = 1; i <= stepCount; i++) {
dates.push(new Date(startDate.getTime() + (stepInterval * i)));
}
return dates;
}
async listActiveCharts() {
const graph = await this.loadGraph();
const charts = graph.entities.filter(e => e.entityType === 'structural_tension_chart');
const chartSummaries = await Promise.all(charts.map(async (chart) => {
const chartId = chart.metadata?.chartId || chart.name.replace('_chart', '');
const progress = await this.getChartProgress(chartId);
// Get desired outcome
const desiredOutcome = graph.entities.find(e => e.name === `${chartId}_desired_outcome` && e.entityType === 'desired_outcome');
return {
chartId,
desiredOutcome: desiredOutcome?.observations[0] || 'Unknown outcome',
dueDate: chart.metadata?.dueDate,
progress: progress.progress,
completedActions: progress.completedActions,
totalActions: progress.totalActions,
level: chart.metadata?.level || 0,
parentChart: chart.metadata?.parentChart
};
}));
return chartSummaries.sort((a, b) => {
// Sort by level first (master charts first), then by due date
if (a.level !== b.level)
return a.level - b.level;
const dateA = new Date(a.dueDate || '').getTime();
const dateB = new Date(b.dueDate || '').getTime();
return dateA - dateB;
});
}
async updateActionProgress(actionStepName, progressObservation, updateCurrentReality) {
const graph = await this.loadGraph();
const actionStep = graph.entities.find(e => e.name === actionStepName && (e.entityType === 'action_step' || e.entityType === 'desired_outcome'));
if (!actionStep) {
throw new Error(`Action step ${actionStepName} not found`);
}
// Add progress observation to action step
actionStep.observations.push(progressObservation);
if (actionStep.metadata) {
actionStep.metadata.updatedAt = new Date().toISOString();
}
// Optionally update current reality with progress
if (updateCurrentReality && actionStep.metadata?.chartId) {
const chartEntity = graph.entities.find(e => e.name === `${actionStep.metadata.chartId}_chart`);
const parentChartId = chartEntity?.metadata?.parentChart;
const targetChartId = parentChartId || actionStep.metadata.chartId;
const currentReality = graph.entities.find(e => e.name === `${targetChartId}_current_reality` &&
e.entityType === 'current_reality');
if (currentReality) {
// Progress observations flow into current reality, changing the structural dynamic
const progressMessage = `Progress on ${actionStep.observations[0]}: ${progressObservation}`;
if (!currentReality.observations.includes(progressMessage)) {
currentReality.observations.push(progressMessage);
if (currentReality.metadata) {
currentReality.metadata.updatedAt = new Date().toISOString();
}
}
}
}
await this.saveGraph(graph);
}
async updateCurrentReality(chartId, newObservations) {
const graph = await this.loadGraph();
const currentReality = graph.entities.find(e => e.name === `${chartId}_current_reality` &&
e.entityType === 'current_reality');
if (!currentReality) {
throw new Error(`Chart ${chartId} not found or missing current reality`);
}
// Add new observations to current reality
const uniqueObservations = newObservations.filter(obs => !currentReality.observations.includes(obs));
currentReality.observations.push(...uniqueObservations);
if (currentReality.metadata) {
currentReality.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraph(graph);
}
async updateDesiredOutcome(chartId, newDesiredOutcome) {
const graph = await this.loadGraph();
const desiredOutcomeEntity = graph.entities.find(e => e.name === `${chartId}_desired_outcome` && e.entityType === 'desired_outcome');
if (!desiredOutcomeEntity) {
throw new Error(`Chart ${chartId} desired outcome not found`);
}
// Replace the first observation (which is the desired outcome text)
desiredOutcomeEntity.observations[0] = newDesiredOutcome;
if (desiredOutcomeEntity.metadata) {
desiredOutcomeEntity.metadata.updatedAt = new Date().toISOString();
}
await this.saveGraph(graph);
}
// Narrative beat creation functionality
async createNarrativeBeat(parentChartId, title, act, type_dramatic, universes, description, prose, lessons, assessRelationalAlignment = false, initiateFourDirectionsInquiry = false, filePath) {
const timestamp = Date.now();
const beatName = `${parentChartId}_beat_${timestamp}`;
// Create narrative beat entity
const entity = {
name: beatName,
entityType: 'narrative_beat',
observations: [
`Act ${act} ${type_dramatic}`,
`Timestamp: ${new Date().toISOString()}`,
`Universe: ${universes.join(', ')}`
],
metadata: {
chartId: parentChartId,
act,
type_dramatic,
universes,
timestamp: new Date().toISOString(),
createdAt: new Date().toISOString(),
narrative: {
description,
prose,
lessons
},
relationalAlignment: {
assessed: false,
score: null,
principles: []
},
fourDirections: {
north_vision: null,
east_intention: null,
south_emotion: null,
west_introspection: null
}
}
};
// Add to graph
await this.createEntities([entity]);
// Create relation to parent chart if it exists
const graph = await this.loadGraph();
const parentChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' && e.metadata?.chartId === parentChartId);
if (parentChart) {
await this.createRelations([{
from: beatName,
to: `${parentChartId}_chart`,
relationType: 'documents',
metadata: {
createdAt: new Date().toISOString(),
description: 'Narrative beat documents chart progress'
}
}]);
}
// TODO: IAIP integration would go here
if (assessRelationalAlignment) {
console.log('š® Relational alignment assessment requested (iaip-mcp integration pending)');
}
if (initiateFourDirectionsInquiry) {
console.log('š§ Four Directions inquiry requested (iaip-mcp integration pending)');
}
return { entity, beatName };
}
async telescopeNarrativeBeat(parentBeatName, newCurrentReality, initialSubBeats) {
const graph = await this.loadGraph();
const parentBeat = graph.entities.find(e => e.name === parentBeatName && e.entityType === 'narrative_beat');
if (!parentBeat) {
throw new Error(`Parent narrative beat not found: ${parentBeatName}`);
}
// Update parent beat's current reality (add to observations)
parentBeat.observations.push(`Telescoped: ${newCurrentReality}`);
if (parentBeat.metadata) {
parentBeat.metadata.updatedAt = new Date().toISOString();
}
const subBeats = [];
// Create sub-beats if provided
if (initialSubBeats && initialSubBeats.length > 0) {
for (let i = 0; i < initialSubBeats.length; i++) {
const subBeat = initialSubBeats[i];
const result = await this.createNarrativeBeat(parentBeatName, // Use parent beat as chart ID
subBeat.title, i + 1, // Sequential act numbers
subBeat.type_dramatic, parentBeat.metadata?.universes || ['engineer-world'], subBeat.description, subBeat.prose, subBeat.lessons);
subBeats.push(result.entity);
}
}
await this.saveGraph(graph);
return { parentBeat, subBeats };
}
async listNarrativeBeats(parentChartId) {
const graph = await this.loadGraph();
const beats = graph.entities.filter(e => e.entityType === 'narrative_beat');
if (parentChartId) {
return beats.filter(beat => beat.metadata?.chartId === parentChartId);
}
return beats;
}
async addActionStep(parentChartId, actionStepTitle, dueDate, currentReality) {
const graph = await this.loadGraph();
const parentChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' && e.metadata?.chartId === parentChartId);
if (!parentChart) {
throw new Error(`Parent chart ${parentChartId} not found`);
}
// Get parent chart's due date for auto-distribution
const parentDueDate = parentChart.metadata?.dueDate;
if (!parentDueDate) {
throw new Error(`Parent chart ${parentChartId} has no due date`);
}
// Calculate due date for action step if not provided
let actionStepDueDate = dueDate;
if (!actionStepDueDate) {
// Distribute between now and parent due date (simple midpoint for now)
const now = new Date();
const parentEnd = new Date(parentDueDate);
const midpoint = new Date(now.getTime() + (parentEnd.getTime() - now.getTime()) / 2);
actionStepDueDate = midpoint.toISOString();
}
// Require current reality assessment - no defaults that prematurely resolve tension
if (!currentReality) {
throw new Error(`š DELAYED RESOLUTION PRINCIPLE VIOLATION
Action step: "${actionStepTitle}"
ā **Problem**: Current reality assessment missing
š **Principle**: "Tolerate discrepancy, tension, and delayed resolution" - Robert Fritz
šÆ **What's Needed**: Honest assessment of your actual current state relative to this action step.
ā
**Examples**:
- "Never used Django, completed Python basics"
- "Built one API, struggling with authentication"
- "Read 3 chapters, concepts still unclear"
ā **Avoid**: "Ready to begin", "Prepared to start", "All set to..."
**Why This Matters**: Premature resolution destroys the structural tension that generates creative advancement. The system NEEDS honest current reality to create productive tension.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
const actionCurrentReality = currentReality;
// Create telescoped structural tension chart
const telescopedChart = await this.createStructuralTensionChart(actionStepTitle, actionCurrentReality, actionStepDueDate);
// Update the telescoped chart's metadata to show parent relationship
const updatedGraph = await this.loadGraph();
const telescopedChartEntity = updatedGraph.entities.find(e => e.name === `${telescopedChart.chartId}_chart`);
if (telescopedChartEntity && telescopedChartEntity.metadata) {
telescopedChartEntity.metadata.parentChart = parentChartId;
telescopedChartEntity.metadata.level = (parentChart.metadata?.level || 0) + 1;
telescopedChartEntity.metadata.updatedAt = new Date().toISOString();
}
// Create relationship: telescoped chart advances toward parent's desired outcome
const parentDesiredOutcome = updatedGraph.entities.find(e => e.name === `${parentChartId}_desired_outcome` && e.entityType === 'desired_outcome');
if (parentDesiredOutcome) {
const timestamp = new Date().toISOString();
await this.createRelations([{
from: `${telescopedChart.chartId}_desired_outcome`,
to: parentDesiredOutcome.name,
relationType: 'advances_toward',
metadata: { createdAt: timestamp }
}]);
}
await this.saveGraph(updatedGraph);
return {
chartId: telescopedChart.chartId,
actionStepName: `${telescopedChart.chartId}_desired_outcome`
};
}
// Enhanced method for LLMs to telescope with intelligent current reality extraction
async telescopeActionStepWithContext(parentChartId, actionStepTitle, userContext, currentReality, dueDate) {
// If current reality not provided, try to extract from context
let finalCurrentReality = currentReality;
if (!finalCurrentReality) {
finalCurrentReality = this.extractCurrentRealityFromContext(userContext, actionStepTitle) ?? undefined;
}
// If still no current reality, provide guidance while maintaining tension
if (!finalCurrentReality) {
throw new Error(`Current reality assessment needed for "${actionStepTitle}". ` +
`Please assess your actual current state relative to this action step. ` +
`Example: "I have never used Django before" or "I completed the basics but haven't built a real project" ` +
`rather than assuming readiness. Structural tension requires honest current reality assessment.`);
}
// Proceed with telescoping using the assessed current reality
return this.addActionStep(parentChartId, actionStepTitle, dueDate, finalCurrentReality);
}
// Unified interface for managing action steps - handles both creation and expansion
async manageActionStep(parentReference, actionDescription, currentReality, initialActionSteps, dueDate) {
const graph = await this.loadGraph();
// Pattern detection: Determine if parentReference is entity name or chart ID
const actionStepPattern = /^chart_\d+_action_\d+$/;
const desiredOutcomePattern = /^chart_\d+_desired_outcome$/;
const chartIdPattern = /^chart_\d+$/;
const isActionStepEntity = actionStepPattern.test(parentReference);
const isDesiredOutcomeEntity = desiredOutcomePattern.test(parentReference);
const isChartId = chartIdPattern.test(parentReference);
// Route 1: Expanding existing action_step entity (legacy pattern)
if (isActionStepEntity) {
const actionStep = graph.entities.find(e => e.name === parentReference && e.entityType === 'action_step');
if (!actionStep) {
// Provide helpful error with available actions
const allActionSteps = graph.entities
.filter(e => e.entityType === 'action_step')
.map(e => `- ${e.name}: "${e.observations[0]}"`);
throw new Error(`š ACTION STEP ENTITY NOT FOUND
Received: "${parentReference}"
Expected: Valid action_step entity name (e.g., "chart_123_action_1")
Available action steps in memory:
${allActionSteps.length > 0 ? allActionSteps.join('\n') : '(none found)'}
Tip: If creating a new action step, use the parent chart ID instead.`);
}
// Use telescoping logic for legacy action_step entities
const currentRealityToUse = currentReality || "Expanding action step into detailed sub-chart";
const telescopedResult = await this.telescopeActionStep(parentReference, currentRealityToUse, initialActionSteps);
// Transform result to include actionStepName
return {
chartId: telescopedResult.chartId,
actionStepName: `${telescopedResult.chartId}_desired_outcome`
};
}
// Route 2: Expanding existing desired_outcome entity (modern pattern)
if (isDesiredOutcomeEntity) {
const desiredOutcome = graph.entities.find(e => e.name === parentReference && e.entityType === 'desired_outcome');
if (!desiredOutcome || !desiredOutcome.metadata?.chartId) {
throw new Error(`š DESIRED OUTCOME ENTITY NOT FOUND
Received: "${parentReference}"
Expected: Valid desired_outcome entity name (e.g., "chart_123_desired_outcome")
Tip: If creating a new action step, use the parent chart ID instead.`);
}
// Use telescoping logic for desired_outcome entities
const currentRealityToUse = currentReality || "Expanding desired outcome into detailed sub-chart";
const telescopedResult = await this.telescopeActionStep(parentReference, currentRealityToUse, initialActionSteps);
// Transform result to include actionStepName
return {
chartId: telescopedResult.chartId,
actionStepName: `${telescopedResult.chartId}_desired_outcome`
};
}
// Route 3: Creating new action step under parent chart (modern pattern)
if (isChartId) {
// Validate parent chart exists
const parentChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' &&
e.metadata?.chartId === parentReference);
if (!parentChart) {
// Provide helpful error with available charts
const allCharts = graph.entities
.filter(e => e.entityType === 'structural_tension_chart')
.map(e => {
const outcome = graph.entities.find(o => o.name === `${e.metadata?.chartId}_desired_outcome`);
return `- ${e.metadata?.chartId}: "${outcome?.observations[0] || 'Unknown'}"`;
});
throw new Error(`š PARENT CHART NOT FOUND
Received: "${parentReference}"
Expected: Valid chart ID (e.g., "chart_123")
Available charts in memory:
${allCharts.length > 0 ? allCharts.join('\n') : '(none found)'}
Tip: Use 'list_active_charts' to see all available charts.`);
}
// Enforce delayed resolution principle for new action creation
if (!currentReality) {
throw new Error(`š DELAYED RESOLUTION PRINCIPLE VIOLATION
Action step: "${actionDescription}"
Parent chart: "${parentReference}"
ā **Problem**: Current reality assessment missing
š **Principle**: "Tolerate discrepancy, tension, and delayed resolution" - Robert Fritz
šÆ **What's Needed**: Honest assessment of actual current state relative to this action step.
ā
**Examples**:
- "Never used Django, completed Python basics"
- "Built one API, struggling with authentication"
- "Read 3 chapters, concepts still unclear"
ā **Avoid**: "Ready to begin", "Prepared to start", "All set to..."
**Why This Matters**: Premature resolution destroys structural tension essential for creative advancement.
š” **Tip**: Run 'init_llm_guidance' for complete methodology overview.`);
}
// Create new action step as telescoped chart
return await this.addActionStep(parentReference, actionDescription, dueDate, currentReality);
}
// Route 4: Invalid format - provide comprehensive guidance
throw new Error(`šØ INVALID PARENT REFERENCE FORMAT
Received: "${parentReference}"
Valid formats:
1. Chart ID: "chart_123" ā Creates new action step
2. Action entity: "chart_123_action_1" ā Expands existing legacy action step
3. Desired outcome: "chart_123_desired_outcome" ā Expands existing modern action step
Examples:
- Create new action: manageActionStep("chart_123", "Complete tutorial", "Never used Django")
- Expand existing: manageActionStep("chart_123_action_1", "Complete tutorial", undefined, ["Step 1", "Step 2"])
š” **Tip**: Use 'list_active_charts' to see available charts and their IDs.`);
}
async removeActionStep(parentChartId, actionStepName) {
const graph = await this.loadGraph();
// Find the action step (which is actually a telescoped chart's desired outcome)
const actionStepEntity = graph.entities.find(e => e.name === actionStepName);
if (!actionStepEntity || !actionStepEntity.metadata?.chartId) {
throw new Error(`Action step ${actionStepName} not found`);
}
const telescopedChartId = actionStepEntity.metadata.chartId;
// Verify it belongs to the parent chart
const telescopedChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' &&
e.metadata?.chartId === telescopedChartId &&
e.metadata?.parentChart === parentChartId);
if (!telescopedChart) {
throw new Error(`Action step ${actionStepName} does not belong to chart ${parentChartId}`);
}
// Remove all entities belonging to the telescoped chart
const entitiesToRemove = graph.entities
.filter(e => e.metadata?.chartId === telescopedChartId)
.map(e => e.name);
await this.deleteEntities(entitiesToRemove);
}
}
const knowledgeGraphManager = new KnowledgeGraphManager();
// The server instance and tools exposed to AI models
const server = new Server({
name: "coaia-narrative",
version: "0.1.0",
description: "COAIA Narrative - Structural Tension Charts with Narrative Beat Extension for multi-universe story capture. Extends coaia-memory with relational and ceremonial integration. šØ NEW LLM? Run 'init_llm_guidance' first."
}, {
capabilities: {
tools: {},
},
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
const enabledTools = getEnabledTools();
const allTools = [
{
name: "create_entities",
description: "ADVANCED: Create traditional knowledge graph entities. For structural tension charts, use create_structural_tension_chart or add_action_step instead.",
inputSchema: {
type: "object",
properties: {
entities: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "The name of the entity" },
entityType: { type: "string", description: "The type of the entity" },
observations: {
type: "array",
items: { type: "string" },