feat: MEMANTO integration — persistent memory for cross-session context (Phases 1-4)

Phase 1: Core Client
- New class-memanto-client.php: Singleton PHP client for MEMANTO API v2
  - Health check with 5-min transient caching
  - Agent CRUD (ensure, activate, deactivate sessions)
  - Memory operations (remember, batch_remember, recall, recall_recent)
  - Auto re-activation on expired session tokens (401 retry)

Phase 2: Write-Through Memory Hooks
- New class-memanto-context-enhancer.php: Orchestrates remember/recall
  - Fires on: user message, plan generated/approved/rejected,
    section written, block refined, config saved, session start/end
  - All hooks via do_action() — zero coupling to MEMANTO when disabled

Phase 3: Context Enrichment
- Context builder injects recalled memories into AI prompts
  via build_memanto_context() in build_working_context()
- 3-recall strategy: recent post memories, semantic search, user preferences
- Deduplication by content hash

Phase 4: Cross-Session Restore
- New REST endpoints: /memanto/restore, /memanto/preferences
- restore_session() recalls 15 recent memories + user preferences on editor load
- build_session_restore_message() creates AI-ready system message
- get_user_preferences_for_new_post() extracts tone/audience/length/language
- Frontend: 🧠 Restored badge in status bar with memory count tooltip
- Preference carry-over: auto-fills post config from stored user preferences
- deactivate_session() called on session end (triggers MEMANTO summary)
- Badge clears on new conversation start

Settings UI:
- MEMANTO Context Keeper section with enable toggle, URL, API key, test connection
- Settings registered via class-settings-v2.php + tab-memanto.php view

Graceful degradation: all MEMANTO calls guarded by is_active(),
frontend catches silently, plugin works identically when disabled.
This commit is contained in:
Dwindi Ramadhana
2026-06-08 12:42:04 +07:00
parent 379a72e52d
commit 619d36d3c8
11 changed files with 28226 additions and 18716 deletions

View File

@@ -6,24 +6,24 @@
* @var array $view_data Prepared view data from class-settings-v2.php
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
if (!defined("ABSPATH")) {
exit();
}
// Extract view data for easier access
extract( $view_data );
extract($view_data);
?>
<div class="wrap wpaw-settings-v2-wrap">
<!-- Agentic IDE Split View Layout -->
<div class="wpaw-ide-container d-flex">
<!-- Left Sidebar: Settings Navigation -->
<div class="wpaw-sidebar-nav flex-shrink-0">
<!-- Header inside Sidebar -->
<div class="wpaw-sidebar-header p-3 mb-2 border-bottom border-dark">
<div class="d-flex align-items-center gap-2">
<img src="<?php echo esc_url( WP_AGENTIC_WRITER_URL . 'assets/img/icon.svg' ); ?>"
alt="WP Agentic Writer"
<img src="<?php echo esc_url(WP_AGENTIC_WRITER_URL . "assets/img/icon.svg"); ?>"
alt="WP Agentic Writer"
style="width: 24px; height: 24px; filter: invert(1)">
<h1 class="h6 mb-0 text-white fw-bold">Agentic Writer</h1>
</div>
@@ -39,19 +39,25 @@ extract( $view_data );
<li class="nav-item" role="presentation">
<button class="nav-link active w-100 text-start d-flex align-items-center gap-2" id="general-tab" data-bs-toggle="pill" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">
<i class="bi bi-sliders"></i>
<?php esc_html_e( 'General', 'wp-agentic-writer' ); ?>
<?php esc_html_e("General", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="models-tab" data-bs-toggle="pill" data-bs-target="#models" type="button" role="tab" aria-controls="models" aria-selected="false">
<i class="bi bi-stars"></i>
<?php esc_html_e( 'AI Models', 'wp-agentic-writer' ); ?>
<?php esc_html_e("AI Models", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="local-backend-tab" data-bs-toggle="pill" data-bs-target="#local-backend" type="button" role="tab" aria-controls="local-backend" aria-selected="false">
<i class="bi bi-house-fill"></i>
<?php esc_html_e( 'Local Backend', 'wp-agentic-writer' ); ?>
<?php esc_html_e("Local Backend", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="memanto-tab" data-bs-toggle="pill" data-bs-target="#memanto" type="button" role="tab" aria-controls="memanto" aria-selected="false">
<i class="bi bi-cpu"></i>
<?php esc_html_e("MEMANTO", "wp-agentic-writer"); ?>
</button>
</li>
@@ -59,13 +65,13 @@ extract( $view_data );
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="cost-log-tab" data-bs-toggle="pill" data-bs-target="#cost-log" type="button" role="tab" aria-controls="cost-log" aria-selected="false">
<i class="bi bi-graph-up"></i>
<?php esc_html_e( 'OpenRouter Cost Log', 'wp-agentic-writer' ); ?>
<?php esc_html_e("OpenRouter Cost Log", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="guide-tab" data-bs-toggle="pill" data-bs-target="#guide" type="button" role="tab" aria-controls="guide" aria-selected="false">
<i class="bi bi-book"></i>
<?php esc_html_e( 'Model Guide', 'wp-agentic-writer' ); ?>
<?php esc_html_e("Model Guide", "wp-agentic-writer"); ?>
</button>
</li>
</ul>
@@ -75,7 +81,7 @@ extract( $view_data );
<!-- Right Content Pane: Settings Forms -->
<div class="wpaw-content-pane flex-grow-1 d-flex flex-column h-100">
<form method="post" action="options.php" id="wpaw-settings-form" class="h-100 d-flex flex-column">
<?php settings_fields( 'wp_agentic_writer_settings' ); ?>
<?php settings_fields("wp_agentic_writer_settings"); ?>
<!-- Workflow Pipeline Progress -->
<div class="wpaw-workflow-progress wpaw-workflow-compact mb-4" id="wpaw-workflow-display">
@@ -140,7 +146,7 @@ extract( $view_data );
<h2 class="h4 text-white m-0">General Settings</h2>
<p class="text-secondary small mt-1">Configure global API keys, budget, and content parameters.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-general.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-general.php"; ?>
</div>
<!-- Models Tab -->
@@ -149,7 +155,7 @@ extract( $view_data );
<h2 class="h4 text-white m-0">AI Models</h2>
<p class="text-secondary small mt-1">Select logic engines for different stages of the writing pipeline.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-models.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-models.php"; ?>
</div>
<!-- Local Backend Tab -->
@@ -158,16 +164,25 @@ extract( $view_data );
<h2 class="h4 text-white m-0">Local Backend</h2>
<p class="text-secondary small mt-1">Configure connections to local LM Studio or Ollama instances.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-local-backend.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-local-backend.php"; ?>
</div>
<!-- Cost Log Tab -->
<!-- MEMANTO Tab -->
<div class="tab-pane fade" id="memanto" role="tabpanel" aria-labelledby="memanto-tab">
<div class="mb-4 pb-3 border-bottom border-dark">
<h2 class="h4 text-white m-0">MEMANTO Context Keeper</h2>
<p class="text-secondary small mt-1">Optional persistent memory for your AI writing assistant. The plugin works perfectly without it.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-memanto.php"; ?>
</div>
<!-- Cost Log Tab -->
<div class="tab-pane fade" id="cost-log" role="tabpanel" aria-labelledby="cost-log-tab">
<div class="mb-4 pb-3 border-bottom border-dark">
<h2 class="h4 text-white m-0">OpenRouter Cost Analytics</h2>
<p class="text-secondary small mt-1">Track API token usage and expenses across all generations.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-cost-log.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-cost-log.php"; ?>
</div>
<!-- Guide Tab -->
@@ -176,7 +191,7 @@ extract( $view_data );
<h2 class="h4 text-white m-0">Provider Documentation</h2>
<p class="text-secondary small mt-1">Reference materials for selecting the right model constraints.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-guide.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-guide.php"; ?>
</div>
</div>
</div>
@@ -185,18 +200,28 @@ extract( $view_data );
<div class="wpaw-save-bar p-3 border-top border-dark d-flex justify-content-between align-items-center bg-transparent mt-auto sticky-bottom">
<div class="text-secondary small d-flex align-items-center gap-2">
<span class="dashicons dashicons-plugin text-primary"></span>
<?php printf( esc_html__( 'v%s', 'wp-agentic-writer' ), esc_html( WP_AGENTIC_WRITER_VERSION ) ); ?>
<?php printf(
esc_html__("v%s", "wp-agentic-writer"),
esc_html(WP_AGENTIC_WRITER_VERSION),
); ?>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="wpaw-reset-settings">
<?php esc_html_e( 'Reset Defaults', 'wp-agentic-writer' ); ?>
<?php esc_html_e("Reset Defaults", "wp-agentic-writer"); ?>
</button>
<button type="submit" class="btn btn-sm btn-primary px-4 fw-semibold" id="wpaw-save-settings">
<?php
$is_mac = isset( $_SERVER['HTTP_USER_AGENT'] ) && strpos( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ), 'Mac OS' ) !== false;
$cmd_key = $is_mac ? '⌘' : 'Ctrl';
?>
<?php esc_html_e( 'Save Settings', 'wp-agentic-writer' ); ?> <kbd class="ms-1 bg-dark text-white border-0 py-0"><?php echo esc_html( $cmd_key ); ?>+S</kbd>
$is_mac =
isset($_SERVER["HTTP_USER_AGENT"]) &&
strpos(wp_unslash($_SERVER["HTTP_USER_AGENT"]), "Mac OS") !== false;
$cmd_key = $is_mac ? "⌘" : "Ctrl";
?>
<?php esc_html_e(
"Save Settings",
"wp-agentic-writer",
); ?> <kbd class="ms-1 bg-dark text-white border-0 py-0"><?php echo esc_html(
$cmd_key,
); ?>+S</kbd>
</button>
</div>
</div>
@@ -210,7 +235,10 @@ extract( $view_data );
<div id="wpaw-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<span class="me-2">✨</span>
<strong class="me-auto"><?php esc_html_e( 'WP Agentic Writer', 'wp-agentic-writer' ); ?></strong>
<strong class="me-auto"><?php esc_html_e(
"WP Agentic Writer",
"wp-agentic-writer",
); ?></strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="wpaw-toast-message"></div>

View File

@@ -0,0 +1,195 @@
<?php
/**
* Settings Tab: MEMANTO Context Keeper
*
* @package WP_Agentic_Writer
* @since 0.3.0
* @var array $view_data Prepared view data (extracted in layout.php)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Variables from $view_data (extracted in layout.php).
$memanto_enabled = $memanto_enabled ?? false;
$memanto_url = $memanto_url ?? '';
$memanto_moorcheh_key = $memanto_moorcheh_key ?? '';
?>
<div class="row g-4">
<!-- Enable MEMANTO -->
<div class="col-12">
<div class="wpaw-card">
<div class="wpaw-card-body">
<div class="form-check form-switch mb-0">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="memanto_enabled"
name="wp_agentic_writer_settings[memanto_enabled]"
value="1"
<?php checked( $memanto_enabled ); ?>
>
<label class="form-check-label text-white fw-medium" for="memanto_enabled">
<?php esc_html_e( 'Enable MEMANTO Integration', 'wp-agentic-writer' ); ?>
</label>
</div>
<p class="text-secondary small mt-2 mb-0">
When enabled, the AI writing assistant will store and recall memories across sessions using MEMANTO.
The plugin works perfectly without MEMANTO — this is an optional enhancement.
</p>
</div>
</div>
</div>
<!-- MEMANTO Instance URL -->
<div class="col-md-6">
<div class="wpaw-card h-100">
<div class="wpaw-card-body">
<label for="memanto_url" class="form-label text-white fw-medium">
<i class="bi bi-link-45deg me-1"></i>
<?php esc_html_e( 'MEMANTO Instance URL', 'wp-agentic-writer' ); ?>
</label>
<input
type="url"
class="form-control"
id="memanto_url"
name="wp_agentic_writer_settings[memanto_url]"
value="<?php echo esc_attr( $memanto_url ); ?>"
placeholder="https://your-instance.context.wpagentic.dev"
autocomplete="off"
>
<div class="form-text text-secondary">
The URL of your MEMANTO instance. Provided when you subscribe to MEMANTO Context Keeper.
</div>
</div>
</div>
</div>
<!-- Moorcheh API Key -->
<div class="col-md-6">
<div class="wpaw-card h-100">
<div class="wpaw-card-body">
<label for="memanto_moorcheh_key" class="form-label text-white fw-medium">
<i class="bi bi-key me-1"></i>
<?php esc_html_e( 'Moorcheh API Key', 'wp-agentic-writer' ); ?>
</label>
<input
type="password"
class="form-control"
id="memanto_moorcheh_key"
name="wp_agentic_writer_settings[memanto_moorcheh_key]"
value="<?php echo esc_attr( $memanto_moorcheh_key ); ?>"
placeholder="Enter your Moorcheh API key"
autocomplete="new-password"
>
<div class="form-text text-secondary">
Get a free API key at <a href="https://moorcheh.ai" target="_blank" class="text-info">moorcheh.ai</a> (10K vectors/month included).
Billed to your own Moorcheh account.
</div>
</div>
</div>
</div>
<!-- Connection Status & Test -->
<div class="col-12">
<div class="wpaw-card">
<div class="wpaw-card-body">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div class="d-flex align-items-center gap-3">
<span id="memanto-status-indicator" class="badge rounded-pill bg-secondary">
<?php if ( ! empty( $memanto_url ) && ! empty( $memanto_moorcheh_key ) ) : ?>
<span class="spinner-border spinner-border-sm me-1" role="status"></span> Checking...
<?php else : ?>
Not configured
<?php endif; ?>
</span>
<span id="memanto-status-detail" class="text-secondary small"></span>
</div>
<button
type="button"
class="btn btn-sm btn-outline-info"
id="wpaw-test-memanto"
<?php echo ( empty( $memanto_url ) || empty( $memanto_moorcheh_key ) ) ? 'disabled' : ''; ?>
>
<i class="bi bi-wifi me-1"></i>
<?php esc_html_e( 'Test Connection', 'wp-agentic-writer' ); ?>
</button>
</div>
</div>
</div>
</div>
<!-- Info Box -->
<div class="col-12">
<div class="wpaw-card border-info" style="border-left: 3px solid var(--bs-info);">
<div class="wpaw-card-body">
<div class="d-flex gap-3">
<span class="fs-4">🧠</span>
<div>
<h6 class="text-white mb-2">What MEMANTO does</h6>
<ul class="text-secondary small mb-2 ps-3">
<li>Your AI assistant <strong class="text-white">remembers context</strong> across sessions and browser refreshes</li>
<li>Writing preferences <strong class="text-white">carry over</strong> between posts</li>
<li>Eliminates <strong class="text-white">context loss</strong> when switching between Chat, Planning, and Writing modes</li>
<li>Reduces AI token usage by up to <strong class="text-white">63%</strong> through smart memory recall</li>
</ul>
<p class="text-secondary small mb-0">
Don't have MEMANTO? <a href="https://wpagentic.dev/memanto" target="_blank" class="text-info">Get MEMANTO Context Keeper</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
// Test MEMANTO connection.
$('#wpaw-test-memanto').on('click', function() {
var btn = $(this);
var statusEl = $('#memanto-status-indicator');
var detailEl = $('#memanto-status-detail');
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span> Testing...');
statusEl.removeClass('bg-success bg-danger bg-secondary bg-warning').addClass('bg-warning').html('Testing...');
detailEl.text('');
$.post(wpawSettingsV2.ajaxUrl, {
action: 'wpaw_test_memanto',
nonce: wpawSettingsV2.nonce,
url: $('#memanto_url').val(),
key: $('#memanto_moorcheh_key').val()
}, function(response) {
btn.prop('disabled', false).html('<i class="bi bi-wifi me-1"></i> Test Connection');
if (response.success && response.data.healthy) {
statusEl.removeClass('bg-warning bg-danger bg-secondary').addClass('bg-success').html('🟢 Connected');
var detail = response.data.details || {};
var info = detail.service ? detail.service + ' v' + (detail.version || '?') : 'Healthy';
if (detail.moorcheh_connected) {
info += ' · Moorcheh connected';
}
detailEl.text(info);
} else {
statusEl.removeClass('bg-warning bg-success bg-secondary').addClass('bg-danger').html('🔴 Error');
detailEl.text(response.data?.message || 'Connection failed');
}
}).fail(function() {
btn.prop('disabled', false).html('<i class="bi bi-wifi me-1"></i> Test Connection');
statusEl.removeClass('bg-warning bg-success bg-secondary').addClass('bg-danger').html('🔴 Error');
detailEl.text('Request failed');
});
});
// Auto-test on load if both fields are filled.
if ($('#memanto_url').val() && $('#memanto_moorcheh_key').val()) {
$('#wpaw-test-memanto').trigger('click');
}
});
</script>