8 Commits

Author SHA1 Message Date
Dwindi Ramadhana
d3f142222c feat: Add connection test caching and reasoning content support
Backend improvements:
- Add cache auto-clear on settings save (class-settings-v2.php)
  - Hooks updated_option action
  - Clears connection test transients when local backend settings change
- Add reasoning_content streaming support (class-local-backend-provider.php)
  - Handles thinking models like Claude extended thinking
  - Captures chunk['choices'][0]['delta']['reasoning_content']

Documentation:
- Add FRONTEND_AND_CHAT_FIX_SUMMARY.md with all fixes
- Add FRONTEND-REFACTOR-PHASE2.md with modularization plan

Note: Splitting effort deferred - will continue iterating on monolith
Next: Fix session history not appearing bug
2026-06-17 05:26:12 +07:00
Dwindi Ramadhana
f55acd7d26 Add sidebar 1:1 migration docs 2026-06-15 11:47:03 +07:00
Dwindi Ramadhana
619d36d3c8 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.
2026-06-08 12:42:04 +07:00
Dwindi Ramadhana
379a72e52d fix: writing stuck - handle empty model response + no-divider fallback + timeline cleanup
Root causes of writing getting stuck:
1. Model returns empty response for a section → now detected early with
   actionable error message including model name
2. Model responds but without ~~~ARTICLE~~~ divider (happens with fallback
   models like Gemini) → now treats entire response as markdown content
3. Stream ends without 'complete' event (error/exit in PHP) → JS timeline
   entries lingered as 'active' forever. Now deactivated on stream close.
4. Error messages in execution flow now use structured formatAiErrorMessage
   with retry button instead of raw text

Also: deactivateActiveTimelineEntries called in catch block so errors
properly clear the 'Writing section X' status indicator.
2026-06-06 05:30:12 +07:00
Dwindi Ramadhana
23a34b3035 fix: session history not loading + new session showing stale messages
Bug 1 - Session opens with empty messages:
- loadChatHistory effect was re-running on every currentSessionId change,
  racing with openSessionById and overwriting loaded messages
- Removed currentSessionId from effect dependencies (only runs on mount/postId)
- Added recovery: if session has 0 messages but has post_id, try fetching
  from the post-based conversation endpoint as fallback

Bug 2 - Start New Session shows old messages:
- startNewConversation now sets isHydratingSessionRef=true before changing
  session state, preventing the persistence effect from saving stale data
- Fully resets: messages, plan, agentMode, keyword suggestions, providerInfo
- loadPostSessions called AFTER state reset to avoid stale renders

Also fixed:
- Legacy fallback now only fires when no session was resolved at all
  (prevents loading old post_meta data over session data)
2026-06-06 05:14:34 +07:00
Dwindi Ramadhana
b4ea9025b1 fix: session persistence + h3 readability + outline error messages
Session issues fixed:
- Removed auto-draft-only gate for showing unassigned sessions on new posts
  (now shows on any post that has no linked sessions)
- Auto-link unassigned session to current post when user opens it
- Added beforeunload/pagehide flush to persist messages before page close
  (prevents data loss from 700ms debounce not firing)
- Added warning log when session loads with 0 messages for debugging

UI fix:
- Override WP editor h3 shrinkage (11px/uppercase → 15px/normal/white)
- Fix h2/h4-h6 headings in response content for dark theme readability

Outline error messages:
- Separate empty response from parse failure with distinct actionable messages
- Show model name + token count on empty response for debugging
- Reassure user that parse failures are usually one-time issues
2026-06-06 00:58:08 +07:00
Dwindi Ramadhana
f7bf1f5153 fix: UX audit improvements - dark theme, structured errors, heartbeat, health check
Phase 1 - UI Theme Consistency:
- Chat messages now use consistent dark theme (removed jarring white bg)
- Plan cards restyled with rounded borders, fills, colored status badges
- Timeline entries use humanist sans-serif instead of monospace
- Error messages now structured (icon + title + detail + action link)
- Input area unified with dark theme cohesion

Phase 2 - UX Flow:
- Added contextual placeholder text per agent mode in textarea
- Added visual mode indicator badge (Chat/Planning/Writing)
- Simplified welcome screen (single 'Continue' + collapsible history)
- Added slash command/mention discovery hint in empty input
- Added write confirmation when editor has existing content
- Added 30s streaming heartbeat (reassurance when model is slow)

Phase 3 - Error Handling:
- Added DB table health check on sidebar init
- Improved 'no API key' error with settings link
- Shows in-chat warning when provider fallback triggers
- Auto-fallback to registry fallback model on unavailability
- isLoading always resets via try/finally pattern
2026-06-06 00:43:10 +07:00
Dwindi Ramadhana
ae70e4aea9 checkpoint: pre-audit baseline state 2026-06-06 00:29:10 +07:00
49 changed files with 32366 additions and 19046 deletions

302
FRONTEND-REFACTOR-PHASE2.md Normal file
View File

@@ -0,0 +1,302 @@
# Frontend Refactor Phase 2: Modularization Plan
**Date**: 2026-06-17
**Status**: 📋 IN PROGRESS
**Author**: Agent
**Reference**: `FRONTEND_AND_CHAT_FIX_SUMMARY.md`
---
## Session Summary (2026-06-17)
### Completed Backend Improvements
| Task | File | Status |
|------|------|--------|
| Connection test caching | `class-provider-manager.php` | ✅ Already existed |
| Cache auto-clear on settings save | `class-settings-v2.php` | ✅ Added |
| Reasoning content parsing | `class-local-backend-provider.php` | ✅ Added |
### Files Modified This Session
1. **`includes/class-settings-v2.php`**
- Added `clear_local_backend_cache_on_settings_change()` method
- Hooked to `updated_option` action
- Clears connection test transients when local backend settings change
2. **`includes/class-local-backend-provider.php`**
- Added `reasoning_content` streaming support (lines ~494-520)
- Handles thinking models like Claude extended thinking
- Debug logging included
### Validation Results
| Check | Result |
|-------|--------|
| `npm run build` | ✅ Passes |
| PHP syntax (all modified files) | ✅ Passes |
| Build output | `dist/sidebar.js` (169 KB) |
---
## Overview
This document outlines the approach for splitting the monolithic `assets/js/src/index.jsx` (11,793 lines) into modular React components, hooks, and utilities while maintaining the existing build pipeline.
### Key Principle
> The build pipeline (`scripts/build.js` → `assets/js/dist/sidebar.js`) **already works correctly**. We are refactoring the source for maintainability, NOT fixing a broken build.
### Important: Two sidebar.js Files
| File | Source of Truth? | Loaded by PHP? |
|------|------------------|---------------|
| `assets/js/sidebar.js` | ❌ Legacy (webpack, 438KB) | ❌ No |
| `assets/js/dist/sidebar.js` | ✅ Current (esbuild, 169KB) | ✅ Yes |
> **Do NOT confuse the two files.** The legacy `sidebar.js` (438KB) is the original monolithic file that was never modularized. The `dist/sidebar.js` (169KB) is the esbuild-compiled output from `src/index.jsx`.
---
## Current State
### Build Pipeline (Working ✅)
```
assets/js/src/index.jsx
│ esbuild (bundle: true)
│ format: "iife"
│ globalName: undefined (anonymous IIFE)
assets/js/dist/sidebar.js (compiled, served to WordPress) ✅
```
### Source & Output Files
| Path | Lines/Size | Purpose | Status |
|------|------------|---------|--------|
| `src/index.jsx` | 11,793 | React source (to be split) | ✅ Active |
| `dist/sidebar.js` | 169 KB | Compiled output (loaded by PHP) | ✅ Active |
| `sidebar.js` | 438 KB | **Legacy file** (NOT loaded) | ⚠️ Archived |
### Build Validation (2026-06-17) ✅
| Check | Result |
|-------|--------|
| `npm run build` | ✅ Passes (165.8kb output) |
| PHP syntax (settings) | ✅ Passes |
| PHP syntax (provider) | ✅ Passes |
| PHP syntax (provider manager) | ✅ Passes |
| PHP syntax (local backend) | ✅ Passes |
| PHP loads `dist/sidebar.js` | ✅ Confirmed in `class-gutenberg-sidebar.php:217` |
### esbuild Configuration
The `scripts/build.js` uses `bundle: true`, which means:
- All local imports are resolved automatically
- No need for separate bundler config
- Extracted files will compile seamlessly
---
## Proposed Directory Structure
```
assets/js/
├── src/
│ ├── index.jsx # Main entry, imports all modules
│ ├── components/
│ │ ├── ChatTab.jsx # Chat tab content
│ │ ├── ConfigTab.jsx # Configuration tab
│ │ ├── CostTab.jsx # Cost tracking tab
│ │ ├── WelcomeScreen.jsx # Welcome/home screen
│ │ ├── Clarification.jsx # Clarification quiz UI
│ │ ├── AgentWorkspaceCard.jsx
│ │ ├── ContextualAction.jsx
│ │ ├── FocusKeywordBar.jsx
│ │ ├── Messages.jsx # Message rendering
│ │ ├── GlobalStatusBar.jsx
│ │ └── RefineAllModal.jsx
│ ├── hooks/
│ │ ├── useChatHistory.js
│ │ ├── useSessionLock.js
│ │ ├── usePostConfig.js
│ │ ├── useWritingState.js
│ │ └── useStreaming.js
│ ├── utils/
│ │ ├── api.js # REST API helpers
│ │ ├── blockUtils.js # Block manipulation
│ │ ├── planUtils.js # Plan parsing/building
│ │ ├── streamUtils.js # Streaming utilities
│ │ ├── markdownUtils.js # Markdown rendering
│ │ └── formatUtils.js # Formatting helpers
│ └── styles/
│ └── components.css # Component-specific styles
└── dist/
└── sidebar.js # Compiled output (auto-generated)
```
---
## Extraction Strategy
### Phase 1: Extract Utilities (Low Risk)
Begin with pure functions that have no React dependencies:
1. **`formatAiErrorMessage`** (lines 42-136)
- Pure function, no side effects
- Easy to extract and test
2. **`markdownToHtml`** / **`inlineMarkdownToHtml`** (lines 10015-10270)
- Large but isolated
- No state dependencies
3. **Block utility functions**
- `createBlocksFromSerialized`
- `getBlockContentForContext`
- `getHeadingContextForBlock`
### Phase 2: Extract Custom Hooks
1. **`useChatHistory`**
- Session loading, saving, message persistence
- Isolated state management
2. **`useSessionLock`**
- Tab locking mechanism
- Heartbeat management
3. **`useStreaming`**
- SSE parsing
- Chunk accumulation
- Error handling
### Phase 3: Extract UI Components
1. **Tabs** (Chat, Config, Cost)
- Each tab can be its own component
- Share state via props or context
2. **WelcomeScreen**
- Self-contained, minimal dependencies
3. **Clarification Quiz**
- Complex but isolated UI
### Phase 4: Extract Editor Integration
The most complex part - interactions with WordPress block editor:
- Block mutation observation
- Input blocking
- Undo/redo integration
---
## Migration Pattern
### Before (in index.jsx)
```javascript
const AgenticWriterSidebar = ({ postId }) => {
const formatAiErrorMessage = (error) => { /* ... */ };
const sendMessage = async (msg) => { /* ... */ };
return <div>...</div>;
};
```
### After (modular)
**`src/utils/formatUtils.js`**
```javascript
export const formatAiErrorMessage = (error, fallback, settings) => {
// ...
};
```
**`src/hooks/useChatApi.js`**
```javascript
export const useChatApi = () => {
const sendMessage = async (msg) => { /* ... */ };
return { sendMessage };
};
```
**`src/index.jsx`**
```javascript
import { formatAiErrorMessage } from './utils/formatUtils';
import { useChatApi } from './hooks/useChatApi';
import { ChatTab } from './components/ChatTab';
const AgenticWriterSidebar = ({ postId }) => {
const { sendMessage } = useChatApi();
return <ChatTab onSend={sendMessage} />;
};
```
---
## Verification Checklist
After each extraction:
- [ ] `npm run build` completes without errors
- [ ] Compiled `sidebar.js` matches expected size (±5%)
- [ ] Chat functionality works (send message, receive response)
- [ ] Planning functionality works (generate plan)
- [ ] Refinement works (refine blocks)
- [ ] Tab switching works
- [ ] Session persistence works
- [ ] No console errors in browser
---
## Rollback Plan
If extraction causes issues:
1. Revert the specific extracted file
2. Keep extracted utilities (safe to keep)
3. Re-run `npm run build`
4. Verify functionality
---
## Next Action
1. **Create `src/utils/` directory**
2. **Extract `formatAiErrorMessage`** as the first migration
3. **Verify build and functionality**
4. **Iterate with next extraction**
---
## Dependencies
| Tool | Status |
|------|--------|
| esbuild | ✅ Configured |
| npm | ✅ Available |
| WordPress environment | ✅ Local by Flywheel |
---
## Questions to Resolve Before Starting
1. Should extracted components use TypeScript or remain JSX?
2. Should we add PropTypes for component prop validation?
3. Should we maintain backward compatibility with `wpAgenticWriter` global?
---
## References
- `FRONTEND_AND_CHAT_FIX_SUMMARY.md` - Original fix documentation
- `scripts/build.js` - Current esbuild configuration
- `assets/js/dist/sidebar.js` - **Compiled output** (loaded by PHP, 169KB)
- `assets/js/sidebar.js` - **Legacy file** (archived, 438KB, NOT loaded)
- `assets/js/src/index.jsx` - Current source (to be split, 11,793 lines)

View File

@@ -0,0 +1,122 @@
# WP Agentic Writer Fix Summary: Chat & Frontend Build
**Date**: 2026-06-16 (original) / 2026-06-17 (updates)
**Status**: 🟢 RESOLVED
This document serves as a hands-off record of the debugging steps, root causes, and fixes applied to resolve the "empty chat response" issue and establish a proper frontend build pipeline.
---
## Updates (2026-06-17)
### Additional Backend Improvements
1. **Connection Test Caching** - Already existed in `class-provider-manager.php` (5-min TTL via transients)
2. **Cache Auto-Clear on Settings Save** - Added to `class-settings-v2.php`
- Hooks to `updated_option` action
- Clears connection test cache when local backend settings change
3. **Reasoning Content Parsing** - Added to `class-local-backend-provider.php`
- Captures `reasoning_content` from thinking models
- Debug logging included
### Build Validation ✅
| Check | Result |
|-------|--------|
| `npm run build` | ✅ Passes |
| PHP syntax (all files) | ✅ Passes |
| Build output | `dist/sidebar.js` (169 KB) |
### Important: Two sidebar.js Files
| File | Loaded by PHP? |
|------|----------------|
| `assets/js/sidebar.js` (438 KB) | ❌ Legacy, NOT loaded |
| `assets/js/dist/sidebar.js` (169 KB) | ✅ Current, loaded |
---
## Original Fixes (2026-06-16)
---
## 1. Frontend Build Pipeline Established
**Problem**:
The PHP plugin explicitly loads `assets/js/dist/sidebar.js`. However, ongoing refactoring work was occurring in `assets/js/src/index.jsx`. Because there was no active build process, changes made in `index.jsx` were not reflecting in the application.
**Solution**:
* Created `package.json` with `esbuild` to handle fast JSX compilation.
* Added `scripts/build.js` configured specifically for WordPress Gutenberg (using `wp.element.createElement` and `wp.element.Fragment`).
* **Critical Fix**: Ensured the esbuild configuration wraps the output in an anonymous IIFE (`format: "iife"`) *without* defining a `globalName: "wp"`. A named global would have caused the compiled script to overwrite WordPress's native `window.wp` object with `undefined`, breaking the editor.
**Usage**:
* Run `npm run build` to compile `src/index.jsx` -> `dist/sidebar.js`.
* Run `npm run build:watch` during active development.
---
## 2. Backend Fix: PHP Fatal Error on Connection Test
**Problem**:
When the local backend provider received an HTTP 4xx/5xx error (e.g., when trying to test a connection to a rate-limited or failing model), the entire REST request crashed, resulting in a 500 server error and breaking the chat flow entirely.
**Root Cause**:
In `includes/class-local-backend-provider.php` (line 882), a double-quoted string was used for a `sprintf` format:
`__("API Error (HTTP %1$d): %2$s", "wp-agentic-writer")`
Because it was double-quoted, PHP attempted to interpolate `$d` and `$s` as variables, resolving them to empty strings. The resulting string `API Error (HTTP %1): %2` caused `sprintf` to throw a fatal `ValueError: Unknown format specifier ")"`.
**Fix**:
Changed the double quotes to single quotes to prevent PHP variable interpolation, preserving the intended positional specifiers:
`__('API Error (HTTP %1$d): %2$s', "wp-agentic-writer")`
*Scanned the rest of the codebase and confirmed this dangerous pattern does not exist anywhere else.*
---
## 3. Backend Fix: Agentic Models Returning "Empty Responses"
**Problem**:
The user experienced intermittent "empty chat response" errors even when the API connection was successful (HTTP 200).
**Root Cause**:
The configured models (e.g., `dough/kr/claude-sonnet-4.5-thinking` and `ag/gemini-3-flash-agent`) are **agentic/tool-calling** models. When fed a large WordPress system prompt asking for plain prose, these models burned all their tokens on internal "reasoning", attempted to emit a function/tool call, failed (`finish_reason: "malformed_function_call"`), and returned **zero text content**.
Because the streaming buffer received no content, it fell back to non-streaming, which also yielded zero content, triggering a generic "empty response" error.
**Fix**:
1. **Better Error Surfacing**: Updated the streaming and fallback logic in `class-local-backend-provider.php` to actively capture the `finish_reason` payload.
2. If the provider returns empty content but has a finish reason of `malformed_function_call`, `tool_calls`, or `function_call`, the backend now intercepts this and throws a highly specific, actionable error:
> *"The selected model [Model Name] returned no text (finish reason: tool/function call). This usually means an agentic/coding model is being used for prose. Choose a standard chat model (without an -agent or -agentic suffix) in Settings."*
3. **Config Alignment**: Used WP-CLI to update the WordPress options table, switching the local backend models from the agentic variants to the standard `gemini/gemini-3-flash-preview` for all prose tasks (`chat`, `writing`, `planning`, etc.).
---
## Summary of Touched Files
* `package.json` (New)
* `scripts/build.js` (New)
* `assets/js/dist/sidebar.js` (Rebuilt)
* `includes/class-local-backend-provider.php` (Fixed string interpolation, added `finish_reason` edge-case handling, added error payload parsing).
The chat functionality is now robust, gracefully handles agentic model failures, and the React frontend can be reliably compiled from `index.jsx`.
---
## 4. Next Recommendations
With the frontend build pipeline established and the critical backend crashes resolved, the foundation is stable. Here are the recommended next steps:
1. **Complete the React File Splitting (Refactor Phase 2)** ⚠️ IN PROGRESS
* Now that `index.jsx` compiles successfully to `dist/sidebar.js`, the massive 11,000+ line `index.jsx` should be split into modular React components.
* **See `FRONTEND-REFACTOR-PHASE2.md` for the detailed modularization plan.**
* The new `esbuild` setup natively supports resolving local imports, meaning you can safely extract components into a `src/components/` directory and import them into `index.jsx` without changing the PHP backend.
* **Key clarification**: The source is `src/index.jsx`, which compiles to `dist/sidebar.js`. We are NOT creating a new `sidebar.jsx` source file - we are modularizing the existing `index.jsx`.
2. **Optimize `test_connection()` in the Provider Manager** ✅ COMPLETED
* Implemented connection test caching using WordPress transients with 5-minute TTL in `class-provider-manager.php`.
* Added automatic cache clearing in `class-settings-v2.php` when local backend settings are saved (URL, API key, model changes).
* This eliminates redundant connection tests on every chat request, significantly reducing latency.
3. **Handle Reasoning Tokens for Thinking Models** ✅ COMPLETED
* Added `reasoning_content` parsing to `chat_stream()` method in `class-local-backend-provider.php`.
* The streaming parser now captures `chunk["choices"][0]["delta"]["reasoning_content"]` from thinking models like Claude extended thinking.
* Reasoning content is passed through the callback so the frontend can optionally display it in a collapsible section.
* Debug logging added to help identify when reasoning chunks are received.

768
SIDEBAR_1_TO_1_MIGRATION.md Normal file
View File

@@ -0,0 +1,768 @@
# WP Agentic Writer Sidebar 1:1 Migration Plan
**Source of truth:** `assets/js/sidebar.js`
**Source length:** 12,363 lines
**Scope:** planning/audit document only. No implementation code, no build step, no inferred behavior.
This document exists to make the migration match `sidebar.js` exactly. If this file, `MIGRATION_GUIDE.md`, comments, or memory disagree with `assets/js/sidebar.js`, `assets/js/sidebar.js` wins.
## Non-Negotiable Rules
1. Preserve every behavior from `assets/js/sidebar.js` before extracting or improving anything.
2. Convert `wp.element.createElement(...)` to JSX mechanically only after copying the same source range.
3. Preserve hook order, state names, ref names, default values, dependency arrays, effect cleanups, and callback boundaries.
4. Preserve all `wpAgenticWriter` globals, endpoint paths, request bodies, headers, nonces, stream handling, abort handling, and error formatting.
5. Preserve all Gutenberg APIs: `registerPlugin`, `PluginSidebarMoreMenuItem`, `PluginSidebar`, `Panel`, `TextareaControl`, `TextControl`, `CheckboxControl`, `Button`, `RawHTML`, `dispatch`, `select`, and `wp.data.withSelect`.
6. Preserve all DOM tags, component tags, `className` values, dynamic class branches, `role`, `aria-*`, `title`, `placeholder`, `style`, `dangerouslySetInnerHTML`, button labels, visible text, icons, SVG markup, and conditional render gates.
7. Preserve editor block attribute class mutations, especially `wpaw-diff-added` and `wpaw-diff-removed`; these are migration-critical even though they are not sidebar wrapper classes.
8. Do not rename functions, split files, normalize copy, deduplicate logic, replace SVGs, replace `RawHTML`, or change UX flow during the first migration pass.
9. Extraction is allowed only after a monolithic JSX port can be checked against this document line range by line range.
## File-Level Boundaries
| Lines | Required migration unit |
| --- | --- |
| 1-6 | File header comment. Preserve package context if the migrated file keeps source banner comments. |
| 7-15 | IIFE argument and WordPress dependency destructuring. New module imports may replace destructuring only if every dependency maps 1:1. |
| 16-30 | Debug logger and `isDebug` behavior. |
| 31-35 | `pluginIcon` image element with `wpAgenticWriter.pluginUrl + "/assets/img/icon.svg"`, alt text, and 20px style. |
| 37-12347 | `AgenticWriterSidebar` component. |
| 12349-12352 | `mapSelectToProps`, selecting `core/editor`. |
| 12355-12356 | `ConnectedSidebar = wp.data.withSelect(mapSelectToProps)(AgenticWriterSidebar)`. |
| 12358-12362 | `registerPlugin("wp-agentic-writer", { icon: pluginIcon, render: ConnectedSidebar })`. |
| 12363 | IIFE close with `window.wp`. |
## Migration Method
1. Create the new JSX target as a monolith first.
2. Copy ranges in the same order as the coverage table below.
3. Convert element calls mechanically:
- tag/component name stays the same
- prop names stay the same
- children order stays the same
- conditional gates stay in the same location
- spread/rest behavior is not introduced unless already present in the source range
4. After the monolith is complete, compare it against this document:
- every range is present
- every function exists
- every state/ref/effect exists in the same hook order
- every class in the class inventory exists
- every endpoint in the endpoint inventory exists
- every render surface exists with the same branches
5. Only after the monolith is proven equivalent, optional extraction may start. Extraction must move code, not rewrite behavior.
## Component-Level Coverage
### State and Refs, Lines 40-366
| Lines | Kind | Name |
| --- | --- | --- |
| 40-41 | const | `settings` |
| 42-138 | const | `formatAiErrorMessage` |
| 139-141 | state | `[activeTab, setActiveTab]` |
| 142-142 | state | `[messages, setMessages]` |
| 143-143 | state | `[input, setInput]` |
| 144-144 | state | `[isLoading, setIsLoading]` |
| 145-145 | state | `[currentSessionId, setCurrentSessionId]` |
| 146-146 | state | `[availableSessions, setAvailableSessions]` |
| 147-148 | state | `[isSessionActionLoading, setIsSessionActionLoading]` |
| 149-151 | state | `[agentMode, setAgentMode]` |
| 152-154 | const/ref | `tabIdRef` |
| 155-159 | state | `[sessionLock, setSessionLock]` |
| 160-162 | const/ref | `lockHeartbeatRef` |
| 163-181 | const/memo | `defaultPostConfig` |
| 182-182 | state | `[postConfig, setPostConfig]` |
| 183-183 | state | `[isConfigLoading, setIsConfigLoading]` |
| 184-184 | state | `[isConfigSaving, setIsConfigSaving]` |
| 185-185 | state | `[configError, setConfigError]` |
| 186-186 | const/ref | `configHydratedRef` |
| 187-187 | const/ref | `lastSavedConfigRef` |
| 188-190 | const/ref | `configSaveTimeoutRef` |
| 191-195 | state | `[cost, setCost]` |
| 196-200 | state | `[monthlyBudget, setMonthlyBudget]` |
| 201-203 | state | `[providerInfo, setProviderInfo]` |
| 204-223 | const | `applyProviderMetadata` |
| 224-224 | state | `[isEditorLocked, setIsEditorLocked]` |
| 225-225 | state | `[isRefinementLocked, setIsRefinementLocked]` |
| 226-226 | state | `[refiningBlockIds, setRefiningBlockIds]` |
| 227-227 | const/ref | `refinementDecoratedIdsRef` |
| 228-228 | const/ref | `lockedEditableNodesRef` |
| 229-229 | const/ref | `lockedBlockIdsRef` |
| 230-230 | const | `REFINEMENT_ALL_CONFIRM_THRESHOLD` |
| 231-235 | state | `[refineAllConfirm, setRefineAllConfirm]` |
| 236-236 | const/ref | `refineAllConfirmResolverRef` |
| 237-239 | const/ref | `skipRefineAllConfirmRef` |
| 240-240 | state | `[seoAudit, setSeoAudit]` |
| 241-241 | state | `[isSeoAuditing, setIsSeoAuditing]` |
| 242-242 | state | `[isGeneratingMeta, setIsGeneratingMeta]` |
| 243-245 | state | `[activeSeoFixKey, setActiveSeoFixKey]` |
| 246-246 | state | `[inClarification, setInClarification]` |
| 247-247 | state | `[questions, setQuestions]` |
| 248-248 | state | `[currentQuestionIndex, setCurrentQuestionIndex]` |
| 249-249 | state | `[answers, setAnswers]` |
| 250-250 | state | `[detectedLanguage, setDetectedLanguage]` |
| 251-252 | state | `[clarificationMode, setClarificationMode]` |
| 253-253 | state | `[pendingRefinement, setPendingRefinement]` |
| 254-254 | state | `[pendingEditPlan, setPendingEditPlan]` |
| 255-255 | state | `[pendingDiffBlockIds, setPendingDiffBlockIds]` |
| 256-256 | const/ref | `lastGenerationRequestRef` |
| 257-257 | const/ref | `currentPlanRef` |
| 258-258 | const/ref | `lastExecuteRequestRef` |
| 259-259 | const/ref | `sectionInsertIndexRef` |
| 260-260 | const/ref | `activeSectionIdRef` |
| 261-261 | const/ref | `sectionBlocksRef` |
| 262-262 | const/ref | `blockSectionRef` |
| 263-263 | const/ref | `markdownRendererRef` |
| 264-264 | const/ref | `lastRefineRequestRef` |
| 265-265 | const/ref | `lastChatRequestRef` |
| 266-266 | const/ref | `stopExecutionRef` |
| 267-267 | const/ref | `activeAbortControllerRef` |
| 268-268 | const/ref | `activeReaderRef` |
| 269-273 | const/ref | `activeOperationRef` |
| 274-274 | state | `[executionStopped, setExecutionStopped]` |
| 275-279 | state | `[activeOperation, setActiveOperation]` |
| 280-287 | state | `[writingState, setWritingState]` |
| 288-289 | state | `[isWritingStateLoading, setIsWritingStateLoading]` |
| 290-295 | state | `[workspaceSnapshot, setWorkspaceSnapshot]` |
| 296-307 | state | `[isWorkspaceCollapsed, setIsWorkspaceCollapsed]` |
| 308-323 | const | `toggleAgentWorkspace` |
| 324-325 | state | `[showMentionAutocomplete, setShowMentionAutocomplete]` |
| 326-326 | state | `[mentionQuery, setMentionQuery]` |
| 327-327 | state | `[mentionOptions, setMentionOptions]` |
| 328-328 | state | `[mentionCursorIndex, setMentionCursorIndex]` |
| 329-330 | state | `[showSlashAutocomplete, setShowSlashAutocomplete]` |
| 331-331 | state | `[slashQuery, setSlashQuery]` |
| 332-332 | state | `[slashOptions, setSlashOptions]` |
| 333-333 | state | `[slashCursorIndex, setSlashCursorIndex]` |
| 334-334 | state | `[isTextareaExpanded, setIsTextareaExpanded]` |
| 335-335 | const/ref | `inputRef` |
| 336-338 | const/ref | `streamTargetRef` |
| 339-340 | state | `[focusKeywordSuggestions, setFocusKeywordSuggestions]` |
| 341-341 | state | `[selectedFocusKeyword, setSelectedFocusKeyword]` |
| 342-343 | state | `[showCustomKeywordInput, setShowCustomKeywordInput]` |
| 344-344 | state | `[customKeywordInput, setCustomKeywordInput]` |
| 345-345 | const/ref | `messagesSaveTimeoutRef` |
| 346-346 | const/ref | `lastPersistedMessagesRef` |
| 347-349 | const/ref | `isHydratingSessionRef` |
| 350-350 | state | `[showWelcome, setShowWelcome]` |
| 351-351 | state | `[welcomeKeywordInput, setWelcomeKeywordInput]` |
| 352-354 | state | `[welcomeStartMode, setWelcomeStartMode]` |
| 355-355 | state | `[aiUndoStack, setAiUndoStack]` |
| 356-358 | const | `MAX_UNDO_STACK` |
| 359-365 | state | `[memantoRestore, setMemantoRestore]` |
| 366-366 | const/ref | `memantoRestoreFetchedRef` |
### Effects, Timeline, Editor Locking, Config Helpers, Lines 367-1194
| Lines | Kind | Name |
| --- | --- | --- |
| 367-372 | effect | `useEffect@367` |
| 373-401 | effect | `useEffect@373` |
| 402-448 | const | `savePostConfig` |
| 449-473 | effect | `useEffect@449` |
| 474-499 | effect | `useEffect@474` |
| 500-510 | const | `normalizeWritingState` |
| 511-539 | const | `saveWritingState` |
| 540-550 | const | `persistWritingStatePatch` |
| 551-585 | effect | `useEffect@551` |
| 586-586 | const/ref | `messagesEndRef` |
| 587-589 | const/ref | `messagesContainerRef` |
| 590-595 | effect | `useEffect@590` |
| 596-597 | const | `progressRegex` |
| 598-608 | const | `activeTimelineStatuses` |
| 609-609 | const | `writingTimelineStatuses` |
| 610-621 | const | `findLastActiveTimelineIndex` |
| 622-636 | const | `deactivateActiveTimelineEntries` |
| 637-659 | const | `updateOrCreateTimelineEntry` |
| 660-673 | const | `addActivityTimeline` |
| 674-682 | const | `setActiveOperationState` |
| 683-691 | const | `beginAgentOperation` |
| 692-700 | const | `finishAgentOperation` |
| 701-721 | const | `markActiveOperationStopping` |
| 722-724 | const | `isAbortError` |
| 725-728 | const | `registerActiveReader` |
| 729-742 | callback | `requestRefineAllConfirmation` |
| 743-752 | callback | `resolveRefineAllConfirmation` |
| 753-764 | const | `captureEditorSnapshot` |
| 765-775 | const | `pushUndoSnapshot` |
| 776-812 | const | `undoLastAiOperation` |
| 813-832 | effect | `useEffect@813` |
| 833-841 | effect | `useEffect@833` |
| 842-889 | effect | `useEffect@842` |
| 890-921 | effect | `useEffect@890` |
| 922-981 | effect | `useEffect@922` |
| 982-1019 | effect | `useEffect@982` |
| 1020-1028 | const | `toTextValue` |
| 1029-1031 | const | `updatePostConfig` |
| 1032-1071 | const | `buildPostConfigFromAnswers` |
| 1072-1079 | const | `handleFocusKeywordChange` |
| 1080-1089 | const | `handleKeywordSelect` |
| 1090-1137 | const | `extractFocusKeywordSuggestions` |
| 1138-1142 | const | `extractFocusKeywordSuggestion` |
| 1143-1153 | const | `addFocusKeywordSuggestion` |
| 1154-1159 | const | `addFocusKeywordSuggestions` |
| 1160-1168 | effect | `useEffect@1160` |
| 1169-1175 | effect | `useEffect@1169` |
| 1176-1194 | const | `handleWelcomeStart` |
### SEO, Workspace, Session, Locking, Lines 1195-2515
| Lines | Kind | Name |
| --- | --- | --- |
| 1195-1269 | const | `runSeoAudit` |
| 1270-1308 | const | `buildSeoAuditFixInstruction` |
| 1309-1310 | const | `getSeoFixKey` |
| 1311-1318 | const | `getSeoAuditPatternCount` |
| 1319-1322 | const | `formatCountLabel` |
| 1323-1328 | const | `formatAuditPatternLabel` |
| 1329-1348 | const | `buildAuditRefinementContext` |
| 1349-1459 | const | `handleSeoAuditFix` |
| 1460-1563 | const | `generateMetaDescription` |
| 1564-1587 | const | `extractBlockPreview` |
| 1588-1599 | const | `getBlockPreviewById` |
| 1600-1638 | callback | `buildWorkspaceSnapshot` |
| 1639-1661 | effect | `useEffect@1639` |
| 1662-1668 | effect | `useEffect@1662` |
| 1669-1682 | effect | `useEffect@1669` |
| 1683-1686 | effect | `useEffect@1683` |
| 1687-1702 | effect | `useEffect@1687` |
| 1703-1716 | effect | `useEffect@1703` |
| 1717-1754 | callback | `sanitizeMessagesForStorage` |
| 1755-1781 | callback | `hydrateSessionStateFromMessages` |
| 1782-1839 | callback | `persistSessionMessages` |
| 1840-1840 | const/ref | `messagesRef` |
| 1841-1845 | effect | `useEffect@1841` |
| 1846-1889 | effect | `useEffect@1846` |
| 1890-1926 | callback | `acquireSessionLock` |
| 1927-1947 | callback | `releaseSessionLock` |
| 1948-1961 | callback | `startLockHeartbeat` |
| 1962-1969 | callback | `stopLockHeartbeat` |
| 1970-1994 | effect | `useEffect@1970` |
| 1995-2026 | callback | `takeOverSession` |
| 2027-2072 | effect | `useEffect@2027` |
| 2073-2213 | effect | `useEffect@2073` |
| 2214-2241 | effect | `useEffect@2214` |
| 2242-2300 | effect | `useEffect@2242` |
| 2301-2397 | const | `loadPostSessions` |
| 2398-2515 | const | `openSessionById` |
### Mentions, Commands, Planning, Agent Decisions, Lines 2516-4320
| Lines | Kind | Name |
| --- | --- | --- |
| 2516-2526 | const | `resolveStreamTarget` |
| 2527-2536 | const | `normalizeMentionToken` |
| 2537-2550 | const | `extractMentionsFromText` |
| 2551-2560 | const | `stripMentionsFromText` |
| 2561-2569 | const | `hasTitleMention` |
| 2570-2689 | const | `handleTitleRefinement` |
| 2690-2712 | const | `parseInsertCommand` |
| 2713-2747 | const | `getSlashOptions` |
| 2748-2758 | const | `getBlockIndex` |
| 2759-2777 | const | `resolveTargetBlockId` |
| 2778-2870 | const | `insertRefinementBlock` |
| 2871-3188 | const | `streamGeneratePlan` |
| 3189-3213 | const | `retryLastGeneration` |
| 3214-3228 | const | `retryLastExecute` |
| 3229-3257 | const | `retryLastRefinement` |
| 3258-3473 | const | `retryLastChat` |
| 3474-3534 | const | `createBlockFromPlan` |
| 3535-3543 | const | `normalizePlanActions` |
| 3544-3604 | const | `buildPlanPreviewItem` |
| 3605-3611 | const | `normalizePlanSectionTitle` |
| 3612-3622 | const | `upsertSectionBlock` |
| 3623-3632 | const | `removeSectionBlock` |
| 3633-3673 | const | `loadSectionBlocks` |
| 3674-3696 | const | `saveSectionBlocks` |
| 3697-3709 | const | `ensurePlanTasks` |
| 3710-3743 | const | `getTargetedRefinementBlocks` |
| 3744-3828 | const | `findBestPlanSectionMatch` |
| 3829-3851 | const | `updatePlanSectionStatus` |
| 3852-3893 | const | `findSectionInsertIndex` |
| 3894-3906 | const | `shouldShowWritingEmptyState` |
| 3907-3956 | const | `summarizeChatHistory` |
| 3957-4007 | const | `detectUserIntent` |
| 4008-4027 | const | `buildOptimizedContext` |
| 4028-4067 | const | `handleResetCommand` |
| 4068-4096 | const | `updateOrCreatePlanMessage` |
| 4097-4156 | const | `suggestKeywordsFromPlan` |
| 4157-4172 | callback | `buildChatHistoryPayload` |
| 4173-4186 | callback | `getLastUserMessageText` |
| 4187-4198 | const | `shouldSkipPlanningCompletion` |
| 4199-4218 | const | `getPlanRuntimeSummary` |
| 4219-4221 | const | `getPlanId` |
| 4222-4260 | const | `classifyAgentIntent` |
| 4261-4320 | const | `decideAgentAction` |
### Execution, Refinement, Block Context, Lines 4321-6388
| Lines | Kind | Name |
| --- | --- | --- |
| 4321-4768 | const | `executePlanFromCard` |
| 4769-4794 | const | `handleStopExecution` |
| 4795-4870 | const | `clearChatContext` |
| 4871-4953 | const | `createBlocksFromSerialized` |
| 4954-5056 | const | `reformatBlocks` |
| 5057-5142 | const | `revisePlanFromPrompt` |
| 5143-5240 | const | `applyEditPlan` |
| 5241-5280 | const | `cancelEditPlan` |
| 5281-5304 | const | `formatClarificationContext` |
| 5305-5327 | effect | `useEffect@5305` |
| 5328-5362 | const | `removeDuplicateHeadings` |
| 5363-5390 | const | `getRefineableBlocks` |
| 5391-5420 | const | `getListItemBlocks` |
| 5421-5426 | const | `resolveExplicitListItem` |
| 5427-5442 | const | `getParentListId` |
| 5443-5453 | const | `getBlockContentForContext` |
| 5454-5470 | const | `getHeadingContextForBlock` |
| 5471-5494 | const | `getNearbyParagraphContext` |
| 5495-5503 | const | `getContextFromMentions` |
| 5504-5518 | const | `extractQuotedTermsFromMessage` |
| 5519-5520 | const | `getAllTextRefineableBlocks` |
| 5521-5533 | const | `selectLikelySlangBlocks` |
| 5534-5537 | const | `isAiSlopRequest` |
| 5538-5589 | const | `getAiSlopFindingsForBlock` |
| 5590-5606 | const | `selectLikelyAiSlopBlocks` |
| 5607-5622 | const | `buildContextBlocksForRefinement` |
| 5623-5704 | const | `buildRefinementDiagnosis` |
| 5705-5799 | const | `resolveBlockMentions` |
| 5800-6388 | const | `handleChatRefinement` |
### Chat Input, Send, Clarification, Welcome, Workspace, Lines 6389-9996
| Lines | Kind | Name |
| --- | --- | --- |
| 6389-6455 | render | `renderRefineAllConfirmModal` |
| 6456-6579 | const | `getMentionOptions` |
| 6580-6612 | effect | `useEffect@6580` |
| 6613-6652 | const | `handleInputChange` |
| 6653-6701 | const | `handleKeyDown` |
| 6702-6726 | const | `insertMention` |
| 6727-6758 | const | `insertSlashCommand` |
| 6759-8056 | const | `sendMessage` |
| 8057-8476 | const | `submitAnswers` |
| 8477-8829 | render | `renderClarification` |
| 8830-8916 | const | `startNewConversation` |
| 8917-8959 | const | `deleteConversationSession` |
| 8960-8983 | const | `getSessionDisplayTitle` |
| 8984-8993 | const | `getSessionContinuityLabel` |
| 8994-9004 | const | `getSessionDebugMeta` |
| 9005-9184 | render | `renderWelcomeScreen` |
| 9185-9253 | render | `renderWritingEmptyState` |
| 9254-9459 | render | `renderFocusKeywordBar` |
| 9460-9629 | render | `renderAgentWorkspaceCard` |
| 9630-9632 | alias | `renderContextIndicator = renderAgentWorkspaceCard` |
| 9633-9996 | render/action | `renderContextualAction` |
### Messages, Tabs, Cost, Final Shell, Lines 9997-12363
| Lines | Kind | Name |
| --- | --- | --- |
| 9997-10917 | render | `renderMessages` |
| 10918-11434 | render | `renderConfigTab` |
| 11435-11448 | const | `getAgentStatus` |
| 11449-11585 | render | `renderGlobalStatusBar` |
| 11586-12020 | render | `renderChatTab` |
| 12021-12022 | state | `[costHistory, setCostHistory]` |
| 12023-12050 | const | `refreshCostData` |
| 12051-12057 | effect | `useEffect@12051` |
| 12058-12306 | render | `renderCostTab` |
| 12307-12346 | return | component `Fragment` with menu item, sidebar, panel, and active tab renderer |
| 12347 | close | `AgenticWriterSidebar` close |
| 12349-12363 | boot | `mapSelectToProps`, `ConnectedSidebar`, `registerPlugin`, IIFE close |
## Render Surface Checklist
Each render surface must be migrated as an element tree, not summarized.
| Render surface | Lines | Required branches/elements |
| --- | --- | --- |
| `renderRefineAllConfirmModal` | 6389-6455 | Null when closed; dialog overlay; modal; title; body; `CheckboxControl`; cancel and continue `Button`s; session skip flag behavior. |
| `renderClarification` | 8477-8829 | Null guard; `renderSingleChoice`; custom option textarea; `renderMultipleChoice`; `renderOpenText`; `renderConfigForm`; answer switch; quiz wrapper; progress bar; previous/skip/next-finish buttons. |
| `renderWelcomeScreen` | 9005-9184 | Recent session button; older sessions details list; session open/delete buttons; focus keyword input; chat/planning mode pills; start button. |
| `renderWritingEmptyState` | 9185-9253 | Empty state wrapper; SVG icon; title; paragraph; create outline button with inline SVG; hint paragraph. |
| `renderFocusKeywordBar` | 9254-9459 | Expanded mode branch; compact mode branch; keyword input behavior; suggestions; selected state; cost/provider indicators; expand/collapse controls. |
| `renderAgentWorkspaceCard` | 9460-9629 | Workspace status; collapsed state; context grid; keyword field; conversation/provider summaries; resume card. |
| `renderContextIndicator` | 9630-9632 | Alias to `renderAgentWorkspaceCard`, not a new implementation. |
| `renderContextualAction` | 9633-9996 | Null guard; `create_outline` action object; clarity check; plan generation stream; contextual action card. |
| `renderMessages` | 9997-10917 | Markdown helpers; grouping logic; user messages; AI group; timeline entries; plan cards; edit-plan cards; structured errors; normal response; resume actions. |
| `renderConfigTab` | 10918-11434 | Configuration wrapper; article length; language; tone; experience; image/search toggles; SEO section; meta generation; SEO audit result/fix UI; status descriptions. |
| `renderGlobalStatusBar` | 11449-11585 | Status dot/label; memory badge; undo; sessions; chat; workspace toggle; config; cost icon buttons. |
| `renderChatTab` | 11586-12020 | Lock banners; health notices; welcome/empty/workspace/activity-log gates; command area; hint; textarea; mention/slash autocomplete; search toggle; stop/send buttons; keyboard hints; refine modal. |
| `renderCostTab` | 12058-12306 | Cost header; refresh; cost cards; budget section; warning; action summary table; history table; settings footer link. |
| Main return | 12307-12346 | `Fragment`; `PluginSidebarMoreMenuItem`; `PluginSidebar`; icon title; `Panel`; `wpaw-tab-content-wrapper`; active tab switch. |
## Nested Function Checklist
These nested helpers are easy to lose during component extraction.
| Parent | Lines | Nested item |
| --- | --- | --- |
| `renderClarification` | 8486-8547 | `renderSingleChoice` |
| `renderClarification` | 8550-8577 | `renderMultipleChoice` |
| `renderClarification` | 8580-8597 | `renderOpenText` |
| `renderClarification` | 8600-8723 | `renderConfigForm` |
| `renderClarification` | 8725-8742 | `answerInput` switch |
| `renderContextualAction` | 9636-9966 | `actions.create_outline` object and async `onClick` |
| `renderMessages` | 9998-10006 | `normalizeMessageContent` |
| `renderMessages` | 10007-10014 | `escapeHtml` |
| `renderMessages` | 10015-10031 | `inlineMarkdownToHtml` |
| `renderMessages` | 10032-10270 | `markdownToHtml` |
| `markdownToHtml` | 10112-10117 | `flushParagraph` |
| `markdownToHtml` | 10118-10140 | `flushList` |
| `markdownToHtml` | 10141-10144 | `addListItem` |
| `markdownToHtml` | 10145-10156 | `addDetailToLastItem` |
| `markdownToHtml` | 10158-10166 | `getListType` |
| `renderMessages` | 10271-10276 | `renderMessageContent` |
| `renderMessages` | 10278-10312 | group building and user message branch |
| `renderMessages` | 10313-10482 | AI group and timeline branch |
| `renderMessages` | 10485-10645 | `message.type === "plan"` branch |
| `renderMessages` | 10647-10767 | `message.type === "edit_plan"` branch |
| `renderMessages` | 10769-10848 | `message.type === "error"` branch |
| `renderMessages` | 10850-10909 | default AI response branch |
| `renderConfigTab` | 10989-10990 | language list merging |
| `renderCostTab` | 12059-12079 | budget percent/status and action summary locals |
## Endpoint Inventory
Every endpoint path below is used by `sidebar.js` and must remain present with its existing method, headers, body shape, and response handling.
| Lines | Endpoint |
| --- | --- |
| 379, 411-412 | `/post-config/${postId}` |
| 479, 12026-12027 | `/cost-tracking/${postId}` |
| 519-520, 558 | `/writing-state/${postId}` |
| 1200-1201 | `/seo-audit/${postId}` |
| 1468-1469 | `/generate-meta` |
| 1813-1814, 1874, 1893-1894, 1931-1932, 1979, 1999-2000, 2122-2123, 2308-2309, 2335-2336, 2413-2414, 2479-2480, 4808-4809, 8836-8837, 8926-8927 | conversation endpoints |
| 2143-2144, 2448-2449 | `/conversation/${postId}` and `/conversation/${data.post_id}` |
| 2167-2168 | `/chat-history/${postId}` |
| 2220 | `/memanto/restore?post_id=${postId}` |
| 2612 | `/refine-title` |
| 2897-2898, 7354-7355, 7762-7763, 8158-8159, 9809-9810 | `/generate-plan` |
| 3288, 6922 | `/chat` |
| 3638-3639, 3680 | `/section-blocks/${postId}` and `/section-blocks` |
| 3915-3916 | `/summarize-context` |
| 3963-3964 | `/detect-intent` |
| 4039 | `/clear-context` |
| 4103-4104 | `/suggest-keywords` |
| 4431-4432 | `/execute-article` |
| 4984-4985 | `/reformat-blocks` |
| 5087 | `/revise-plan` |
| 5969-5970 | `/refine-from-chat` |
| 7251-7252, 9681-9682 | `/check-clarity` |
## Editor and Browser Side Effects
Preserve these integrations exactly.
| Lines | Side effect |
| --- | --- |
| 300-314 | `localStorage` key `wpaw_agent_workspace_collapsed`. |
| 449-473 | Debounced config save via `configSaveTimeoutRef`. |
| 683-700, 4769-4794 | `AbortController` lifecycle and active operation state. |
| 813-841 | Post saving lock/unlock for writing and refining. |
| 842-981 | Editor input lock, block decoration, and global key/paste/drop/cut blockers. |
| 1651-1653 | `wp.data.subscribe` workspace snapshot update. |
| 1678-1680, 1846-1889, 1970-1994 | `beforeunload` handlers. |
| 1692, 1708, 2095 | `localStorage` session keys. |
| 1948-1969 | Session lock heartbeat interval. |
| 2871-3188, 3258-3473, 4321-4768, 5800-6388, 6759-8056, 8057-8476, 9633-9996 | Stream readers, `TextDecoder`, timeout cleanup, and abort checks. |
| 6580-6612 | Window event `wpaw:insert-mention`. |
| 9400-9404, 9563-9567 | Focus keyword debounce saves. |
| 12349-12356 | `wp.data.withSelect` connection to current post ID. |
## Class Inventory
All classes below are present in `sidebar.js`; preserve spelling and combinations. First column is the first line where the class appears.
```text
6075 wpaw-diff-removed
6088 wpaw-diff-added
6129 wpaw-diff-added
6397 wpaw-refine-confirm-overlay
6404 wpaw-refine-confirm-modal
6407 wpaw-refine-confirm-title
6412 wpaw-refine-confirm-body
6428 wpaw-refine-confirm-actions
8493 wpaw-answer-options
8515 wpaw-custom-answer-wrapper
8533 wpaw-custom-text-input
8631 wpaw-config-form
8646 wpaw-config-field
8653 wpaw-config-label
8656 wpaw-config-label-text
8662 wpaw-config-description
8668 wpaw-config-toggle
8682 wpaw-toggle-slider
8706 wpaw-config-text-input
8746 wpaw-clarification-quiz
8746 dark-theme
8749 wpaw-quiz-header
8753 wpaw-progress-bar
8755 wpaw-progress-fill
8770 wpaw-question-card
8775 wpaw-quiz-actions
9011 wpaw-welcome-screen
9014 wpaw-welcome-content
9016 wpaw-welcome-icon
9024 wpaw-welcome-title
9029 wpaw-welcome-subtitle
9037 wpaw-welcome-pill
9065 wpaw-session-list
9086 wpaw-session-open-btn
9135 wpaw-welcome-input
9148 wpaw-welcome-pills
9176 wpaw-welcome-start-btn
9188 wpaw-writing-empty-state
9191 wpaw-empty-state-content
9193 wpaw-empty-state-icon
9210 wpaw-empty-state-button
9244 wpaw-empty-state-hint
9262 wpaw-focus-keyword-bar
9262 wpaw-expanded
9266 wpaw-fk-header
9271 wpaw-fk-collapse
9281 wpaw-fk-main-input
9284 wpaw-fk-custom-input
9311 wpaw-fk-suggestions
9314 wpaw-fk-suggestions-label
9322 wpaw-fk-suggestion-item
9324 selected
9329 wpaw-fk-radio
9334 wpaw-fk-suggestion-text
9339 wpaw-fk-suggestion-source
9348 wpaw-fk-stats
9358 wpaw-provider-info
9370 wpaw-fk-divider
9385 wpaw-compact
9388 wpaw-fk-left
9389 wpaw-fk-icon
9392 wpaw-fk-input
9417 wpaw-fk-cost
9423 wpaw-provider-badge
9435 wpaw-fk-expand
9489 wpaw-agent-workspace-card
9489 is-collapsed
9493 wpaw-agent-workspace-header
9496 wpaw-agent-workspace-heading
9499 wpaw-agent-workspace-kicker
9504 wpaw-agent-workspace-title
9510 wpaw-agent-workspace-actions
9514 wpaw-agent-workspace-status
9514 status-${activeWorkspaceStatus}
9523 wpaw-agent-context-grid
9526 wpaw-agent-context-item
9556 wpaw-agent-keyword-input
9598 wpaw-agent-resume-card
9973 wpaw-contextual-action
9976 wpaw-action-icon
9981 wpaw-action-content
10303 wpaw-message
10303 wpaw-message-user
10307 wpaw-message-content
10343 wpaw-ai-response
10352 complete
10355 inactive
10356 active
10372 wpaw-ai-item
10372 wpaw-timeline-entry
10375 is-current
10378 wpaw-timeline-dot
10383 wpaw-timeline-content
10386 wpaw-timeline-message
10393 wpaw-timeline-complete-row
10402 wpaw-timeline-complete
10407 wpaw-timeline-elapsed
10421 wpaw-inline-undo-btn
10452 wpaw-processing-indicator
10454 wpaw-dots-loader
10469 wpaw-typing-indicator
10475 wpaw-typing-dots
10555 wpaw-plan-card
10559 wpaw-plan-title
10564 wpaw-plan-config-summary
10568 wpaw-config-summary-item
10576 wpaw-plan-sections
10582 wpaw-plan-section
10582 pending
10582 done
10582 in_progress
10586 wpaw-plan-section-row
10588 wpaw-plan-section-check
10596 wpaw-plan-section-body
10599 wpaw-plan-section-title
10607 wpaw-plan-section-desc
10613 wpaw-plan-section-status
10633 wpaw-plan-actions
10676 wpaw-edit-plan
10680 wpaw-edit-plan-title
10685 wpaw-edit-plan-summary
10691 wpaw-edit-plan-preview-label
10697 wpaw-edit-plan-list
10703 wpaw-edit-plan-item
10708 wpaw-edit-plan-item-title
10718 wpaw-edit-plan-item-target
10746 wpaw-edit-plan-actions
10797 wpaw-message-error
10805 wpaw-error-title
10812 wpaw-error-detail
10854 wpaw-response
10858 wpaw-response-content
10884 wpaw-resume-actions
10923 wpaw-tab-content
10923 wpaw-config-tab
10927 wpaw-tab-header
10933 wpaw-config-section
10937 description
10952 wpaw-select
11093 wpaw-config-divider
11161 wpaw-meta-info
11168 good
11169 warning
11192 wpaw-spinning-icon
11210 wpaw-svg-wrapper
11226 wpaw-seo-audit
11229 wpaw-seo-audit-header
11285 wpaw-seo-audit-results
11289 wpaw-seo-score
11295 poor
11299 score-value
11304 score-label
11310 wpaw-seo-stats
11313 wpaw-seo-stat
11316 stat-label
11321 stat-value
11343 wpaw-seo-checks
11352 wpaw-seo-check
11354 passed
11354 failed
11358 check-icon
11363 check-label
11372 wpaw-seo-fix-button
11374 is-fixing
11465 wpaw-status-bar
11471 wpaw-status-indicator
11473 wpaw-status-dot
11477 wpaw-status-label
11486 wpaw-memanto-badge
11495 wpaw-status-actions
11499 wpaw-status-icon-btn
11499 wpaw-undo-btn
11499 has-undo
11512 is-active
11544 wpaw-workspace-toggle-btn
11592 wpaw-chat-tab
11596 wpaw-chat-container
11596 is-dimmed
11603 wpaw-editor-lock-banner
11609 wpaw-refinement-lock-banner
11616 wpaw-session-lock-banner
11627 wpaw-session-lock-takeover
11639 wpaw-health-notice
11670 wpaw-messages
11670 wpaw-activity-log
11674 wpaw-messages-inner
11687 wpaw-command-area
11695 wpaw-input-hint
11695 is-hidden
11707 wpaw-command-input-wrapper
11709 expanded
11713 wpaw-command-prefix
11718 wpaw-input
11758 wpaw-mention-autocomplete
11777 wpaw-mention-option
11877 wpaw-command-actions
11881 wpaw-command-actions-group
11903 wpaw-web-search-toggle
11905 wpaw-search-blocked
11931 wpaw-web-search-icon
11939 wpaw-web-search-label
11953 wpaw-command-circle-btn
11954 wpaw-stop-circle-btn
11955 is-stopping
11964 wpaw-stop-spinner
11972 wpaw-send-circle-btn
11986 wpaw-keyboard-hints
11989 wpaw-kbd
12062 ok
12062 danger
12083 wpaw-cost-tab
12089 wpaw-refresh-btn
12100 wpaw-cost-card
12103 wpaw-cost-stat
12107 wpaw-cost-value
12125 wpaw-cost-remaining
12137 wpaw-budget-section
12140 wpaw-budget-label
12156 wpaw-budget-bar
12158 wpaw-budget-fill
12167 wpaw-budget-warning
12176 wpaw-cost-history
12181 wpaw-cost-table-wrapper
12186 wpaw-cost-table
12285 wpaw-cost-footer
12293 wpaw-cost-settings-link
12338 wpaw-tab-content-wrapper
```
## Non-Render Class and Selector Inventory
These names are not all sidebar `className` props, but they are still source-of-truth migration items.
| Lines | Name | Required preservation |
| --- | --- | --- |
| 824, 828 | `wpaw-writing` | WordPress post-saving lock key. |
| 825, 829 | `wpaw-editor-locked` | Body class added/removed while writing lock is active. |
| 835, 838 | `wpaw-refining` | WordPress post-saving lock key. |
| 836, 839 | `wpaw-refining-locked` | Body class added/removed while refinement lock is active. |
| 895, 903, 916 | `wpaw-block-refining` | Editor block DOM class added/removed for refining blocks. |
| 933 | `.wpaw-sidebar` | Selector allowlist for editor input blocking. Preserve alongside `.wpaw-command-area` and `.wpaw-messages`. |
| 5262 | `wpaw-diff-removed` | Removed from block `className` during diff cleanup. |
## Dynamic Class Expressions
These expressions must be migrated as expressions, not flattened.
| Lines | Expression behavior |
| --- | --- |
| 6075-6078, 6111-6114 | Append `wpaw-diff-removed` to editor block `className`. |
| 6085-6089, 6128-6129 | Append `wpaw-diff-added` to generated diff blocks. |
| 9152-9154 | Welcome chat pill adds `active`. |
| 9162-9164 | Welcome planning pill adds `active`. |
| 9322-9324 | Focus keyword suggestion item adds `selected`. |
| 9489 | Workspace card adds `is-collapsed`. |
| 9514 | Workspace status uses `status-${activeWorkspaceStatus}`. |
| 10372-10375 | Timeline entry combines `wpaw-ai-item wpaw-timeline-entry`, `statusClass`, and `is-current`. |
| 10582 | Plan section uses `section.status || "pending"`. |
| 11165-11169 | Meta character count uses `good` or `warning`. |
| 11289-11295 | SEO score uses `good`, `warning`, or `poor`. |
| 11352-11354 | SEO check uses `passed` or `failed`. |
| 11372-11374 | SEO fix button adds `is-fixing`. |
| 11473 | Status dot appends `agentStatus`. |
| 11510-11512 | Sessions icon button adds `is-active`. |
| 11527-11529 | Chat icon button adds `is-active`. |
| 11556-11558 | Config icon button adds `is-active`. |
| 11570-11572 | Cost icon button adds `is-active`. |
| 11596 | Chat container adds `is-dimmed`. |
| 11695 | Input hint adds `is-hidden`. |
| 11707-11709 | Command input wrapper adds `expanded`. |
| 11777-11779 | Mention autocomplete option adds `selected`. |
| 11838-11840 | Slash autocomplete option adds `selected`. |
| 11903-11905 | Web search toggle adds `wpaw-search-blocked`. |
| 11953-11955 | Stop button adds `is-stopping`. |
| 12129, 12158, 12167 | Cost/budget UI appends `budgetStatus`. |
## Migration Acceptance Checklist
- [ ] `sidebar.js` line ranges 1-12363 are represented in the migrated plan or monolith.
- [ ] The component starts from `AgenticWriterSidebar` line 38 and closes at line 12347.
- [ ] Final HOC/plugin registration lines 12349-12363 are represented separately.
- [ ] Every state/ref/effect in the coverage table exists in the same order.
- [ ] Every function in the coverage table exists under the same name.
- [ ] Every nested helper in the nested checklist exists.
- [ ] Every render surface in the render checklist exists with the same branch gates.
- [ ] Every class in the class inventory exists, including dynamic and editor block classes.
- [ ] Every endpoint in the endpoint inventory exists with the same request semantics.
- [ ] Every editor/browser side effect in the side-effect inventory exists.
- [ ] No refactor-only cleanup has been mixed into the first migration pass.

296
SIDEBAR_1_TO_1_TASKLIST.md Normal file
View File

@@ -0,0 +1,296 @@
# WP Agentic Writer Sidebar 1:1 Migration Tasklist
**Source task plan:** `SIDEBAR_1_TO_1_MIGRATION.md`
**Source of truth:** `assets/js/sidebar.js`
**Mode:** planning/tasklist only. No implementation code, no build step, no inferred behavior.
Use this tasklist to execute the migration described in `SIDEBAR_1_TO_1_MIGRATION.md`. Every checkbox must be completed against `assets/js/sidebar.js`, not memory.
**Current status:** planning docs are in place and the abandoned React rebuild scaffold has been removed from `assets/js`. No build was run, no enqueue switch was made, and implementation now needs to restart from the migration plan rather than from a preexisting React target.
## Ground Rules
- [x] Confirm `assets/js/sidebar.js` is the current source of truth before starting.
- [x] Confirm `SIDEBAR_1_TO_1_MIGRATION.md` is open beside `assets/js/sidebar.js`.
- [x] Do not use `MIGRATION_GUIDE.md` as authority when it differs from `assets/js/sidebar.js`.
- [x] Do not rename functions during the first migration pass.
- [x] Do not extract helper files during the first migration pass.
- [x] Do not change visible text, labels, placeholders, icons, SVG markup, classes, inline styles, aria attributes, roles, or conditional render gates.
- [x] Do not replace `RawHTML`, markdown logic, stream handling, abort handling, localStorage keys, endpoint paths, request bodies, or Gutenberg APIs.
- [ ] Keep the first target as a monolithic JSX port.
## Definition of Done for Each Range
- [ ] Source range is copied or represented completely.
- [ ] `createElement` conversion keeps the same tag/component.
- [ ] Props are preserved 1:1.
- [ ] Children order is preserved 1:1.
- [ ] Conditional gates are preserved in the same branch position.
- [ ] State/ref/effect ordering is preserved.
- [ ] Callback boundaries and dependency arrays are preserved.
- [ ] Inline styles, string literals, titles, placeholders, labels, and SVG strings are preserved.
- [ ] Any migrated range is checked against the original source lines before moving on.
## Phase 0 - Source Lock and Boundaries
- [x] Verify `sidebar.js` has 12,363 lines.
- [x] Verify file header and IIFE boundary: lines 1-7 and 12363.
- [x] Verify dependency destructuring: lines 8-15.
- [x] Verify debug logger and `pluginIcon`: lines 16-35.
- [x] Verify `AgenticWriterSidebar` starts at line 38.
- [x] Verify `AgenticWriterSidebar` closes at line 12347.
- [x] Verify HOC/plugin boot remains separate: lines 12349-12363.
- [x] Verify main return starts at line 12307.
- [x] Record any source drift before implementation. If line numbers changed, regenerate this tasklist from the updated source.
## Phase 1 - Monolith Skeleton
- [ ] Create the migrated target as one monolithic sidebar component.
- [ ] Map every WordPress dependency from lines 8-15.
- [ ] Preserve `pluginIcon` behavior from lines 31-35.
- [ ] Preserve component prop shape: `AgenticWriterSidebar = ({ postId })`.
- [ ] Preserve `mapSelectToProps` behavior from lines 12349-12352.
- [ ] Preserve `ConnectedSidebar` behavior from lines 12355-12356.
- [ ] Preserve `registerPlugin("wp-agentic-writer")` behavior from lines 12358-12362.
- [ ] Do not split files yet.
## Phase 2 - State, Refs, and Constants
- [ ] Migrate settings and error formatting: lines 40-138.
- [ ] Migrate chat/top-level state: lines 139-149.
- [ ] Migrate session lock state and refs: lines 152-160.
- [ ] Migrate config defaults, config state, and config refs: lines 163-188.
- [ ] Migrate cost/provider state and provider metadata helper: lines 191-223.
- [ ] Migrate editor/refinement lock state and refs: lines 224-237.
- [ ] Migrate SEO audit state: lines 240-243.
- [ ] Migrate clarification, pending plan, and request refs: lines 246-268.
- [ ] Migrate active operation refs/state: lines 269-279.
- [ ] Migrate writing state: lines 280-289.
- [ ] Migrate workspace snapshot/collapse state and toggle: lines 290-321.
- [ ] Migrate mention/slash/input refs and state: lines 323-336.
- [ ] Migrate focus keyword state and persistence refs: lines 338-347.
- [ ] Migrate welcome state: lines 349-352.
- [ ] Migrate undo stack and max size: lines 354-356.
- [ ] Migrate Memanto restore state/ref: lines 358-366.
- [ ] Verify state/ref/hook order matches `sidebar.js` exactly.
## Phase 3 - Effects, Saving, Timeline, and Editor Locks
- [ ] Migrate agent mode reset effect: lines 367-371.
- [ ] Migrate post config load effect: lines 373-400.
- [ ] Migrate `savePostConfig`: lines 402-448.
- [ ] Migrate debounced post config effect: lines 449-473.
- [ ] Migrate cost tracking effect: lines 474-499.
- [ ] Migrate `normalizeWritingState`: lines 500-510.
- [ ] Migrate `saveWritingState`: lines 511-539.
- [ ] Migrate `persistWritingStatePatch`: lines 540-550.
- [ ] Migrate writing state load effect: lines 551-585.
- [ ] Migrate scroll refs/effect: lines 586-595.
- [ ] Migrate timeline regex/status constants and helpers: lines 596-673.
- [ ] Migrate active operation helpers: lines 674-728.
- [ ] Migrate refine-all confirmation callbacks: lines 729-752.
- [ ] Migrate undo snapshot helpers: lines 753-812.
- [ ] Migrate post saving lock effects: lines 813-841.
- [ ] Migrate editor input lock and DOM blocker effect: lines 842-981.
- [ ] Migrate text/config/keyword helpers and welcome start: lines 1020-1194.
## Phase 4 - SEO, Workspace, Sessions, and Locks
- [ ] Migrate `runSeoAudit`: lines 1195-1269.
- [ ] Migrate SEO fix instruction helpers: lines 1270-1348.
- [ ] Migrate `handleSeoAuditFix`: lines 1349-1459.
- [ ] Migrate `generateMetaDescription`: lines 1460-1563.
- [ ] Migrate block preview helpers: lines 1564-1599.
- [ ] Migrate workspace snapshot helper/effects: lines 1600-1668.
- [ ] Migrate beforeunload/session restore effects: lines 1669-1716.
- [ ] Migrate session storage sanitizing and hydration: lines 1717-1781.
- [ ] Migrate session message persistence: lines 1782-1889.
- [ ] Migrate session lock acquire/release/heartbeat/takeover: lines 1890-2026.
- [ ] Migrate lock/chat-history/post-session effects: lines 2027-2300.
- [ ] Migrate `loadPostSessions`: lines 2301-2397.
- [ ] Migrate `openSessionById`: lines 2398-2515.
## Phase 5 - Mentions, Commands, Plans, and Agent Decisions
- [ ] Migrate stream target and mention token helpers: lines 2516-2569.
- [ ] Migrate title refinement: lines 2570-2689.
- [ ] Migrate slash command parsing/options/block index: lines 2690-2758.
- [ ] Migrate target block resolution and insert refinement block: lines 2759-2870.
- [ ] Migrate plan streaming: lines 2871-3188.
- [ ] Migrate retry helpers: lines 3189-3473.
- [ ] Migrate plan block/preview helpers: lines 3474-3604.
- [ ] Migrate section block helpers and persistence: lines 3605-3696.
- [ ] Migrate plan task/target/matching/status/index helpers: lines 3697-3906.
- [ ] Migrate chat summary, intent detection, context, reset, plan message, keyword suggestion, and runtime helpers: lines 3907-4218.
- [ ] Migrate plan ID, intent classifier, and action decider: lines 4219-4320.
## Phase 6 - Execution, Refinement, and Block Context
- [ ] Migrate `executePlanFromCard`: lines 4321-4768.
- [ ] Migrate stop execution: lines 4769-4794.
- [ ] Migrate clear chat context: lines 4795-4870.
- [ ] Migrate serialized block creation: lines 4871-4953.
- [ ] Migrate block reformatting: lines 4954-5056.
- [ ] Migrate plan revision: lines 5057-5142.
- [ ] Migrate edit plan apply/cancel: lines 5143-5280.
- [ ] Migrate clarification context effect/helper: lines 5281-5327.
- [ ] Migrate duplicate heading removal: lines 5328-5362.
- [ ] Migrate refineable block/list helpers: lines 5363-5442.
- [ ] Migrate context block helpers: lines 5443-5520.
- [ ] Migrate slang/AI-slop detection helpers: lines 5521-5606.
- [ ] Migrate refinement diagnosis and block mention resolution: lines 5607-5799.
- [ ] Migrate `handleChatRefinement`: lines 5800-6388.
- [ ] Verify `wpaw-diff-added` and `wpaw-diff-removed` behavior survives unchanged.
## Phase 7 - Chat Input and Main Async Flows
- [ ] Migrate refine-all modal: lines 6389-6455.
- [ ] Migrate mention options: lines 6456-6579.
- [ ] Migrate custom insert-mention event effect: lines 6580-6612.
- [ ] Migrate input change/key handling: lines 6613-6701.
- [ ] Migrate mention and slash insertion helpers: lines 6702-6758.
- [ ] Migrate `sendMessage`: lines 6759-8056.
- [ ] Migrate `submitAnswers`: lines 8057-8476.
- [ ] Verify all stream readers, decoders, timeouts, active readers, and abort branches match the source.
## Phase 8 - Render Surfaces
- [ ] Migrate `renderClarification`: lines 8477-8829.
- [ ] Verify `renderSingleChoice`: lines 8486-8547.
- [ ] Verify `renderMultipleChoice`: lines 8550-8577.
- [ ] Verify `renderOpenText`: lines 8580-8597.
- [ ] Verify `renderConfigForm`: lines 8600-8723.
- [ ] Verify answer input switch: lines 8725-8742.
- [ ] Migrate conversation session actions: lines 8830-9004.
- [ ] Migrate `renderWelcomeScreen`: lines 9005-9184.
- [ ] Migrate `renderWritingEmptyState`: lines 9185-9253.
- [ ] Migrate `renderFocusKeywordBar`: lines 9254-9459.
- [ ] Migrate `renderAgentWorkspaceCard`: lines 9460-9629.
- [ ] Preserve `renderContextIndicator = renderAgentWorkspaceCard`: lines 9630-9632.
- [ ] Migrate `renderContextualAction`: lines 9633-9996.
- [ ] Migrate `renderMessages`: lines 9997-10917.
- [ ] Migrate `renderConfigTab`: lines 10918-11434.
- [ ] Migrate `getAgentStatus`: lines 11435-11448.
- [ ] Migrate `renderGlobalStatusBar`: lines 11449-11585.
- [ ] Migrate `renderChatTab`: lines 11586-12020.
- [ ] Migrate cost state/effect/helpers: lines 12021-12057.
- [ ] Migrate `renderCostTab`: lines 12058-12306.
- [ ] Migrate main component return tree: lines 12307-12346.
## Phase 9 - Render Surface Element Audit
- [ ] Refine modal: confirm dialog role, aria modal/label, overlay, modal, title, body, checkbox, cancel, continue.
- [ ] Clarification UI: confirm all question types, custom answer field, config fields, progress bar, previous/skip/next/finish behavior.
- [ ] Welcome UI: confirm recent session, older session details, open/delete buttons, keyword input, mode pills, start button.
- [ ] Writing empty state: confirm SVG icon, copy, create outline button, hint text.
- [ ] Focus keyword bar: confirm expanded/compact branches, suggestions, selected state, provider/cost display, expand/collapse.
- [ ] Workspace card: confirm collapsed state, status, context grid, keyword field, conversation/provider summary, resume card.
- [ ] Contextual action: confirm `create_outline` action, clarity check branch, generate-plan stream branch, action card.
- [ ] Messages: confirm markdown helpers, grouping, timeline, plan, edit plan, structured errors, retry buttons, default response, resume actions.
- [ ] Config tab: confirm every section, control, description, SEO controls, meta generation, audit score, check list, fix buttons.
- [ ] Status bar: confirm status dot, memory badge, undo, sessions, chat, workspace toggle, config, cost buttons.
- [ ] Chat tab: confirm lock banners, health notices, welcome/empty/workspace/activity gates, command area, hint, textarea, autocompletes, search toggle, stop/send, keyboard hints, modal.
- [ ] Cost tab: confirm header, refresh, cards, budget section, warning, action summary table, history table, footer link.
- [ ] Final shell: confirm menu item, sidebar title, icon, panel, tab wrapper, active tab branch.
## Phase 10 - Class Inventory Audit
- [ ] Verify every literal `className` token from `SIDEBAR_1_TO_1_MIGRATION.md` exists in the migrated target.
- [ ] Verify dynamic classes remain dynamic expressions.
- [ ] Verify `wpaw-diff-removed` append branch: lines 6075-6078 and 6111-6114.
- [ ] Verify `wpaw-diff-added` append branches: lines 6085-6089 and 6128-6129.
- [ ] Verify welcome pill `active` branches: lines 9152-9154 and 9162-9164.
- [ ] Verify focus suggestion `selected`: lines 9322-9324.
- [ ] Verify workspace `is-collapsed` and `status-${activeWorkspaceStatus}`: lines 9489 and 9514.
- [ ] Verify timeline `statusClass` and `is-current`: lines 10351-10375.
- [ ] Verify plan section `section.status || "pending"`: line 10582.
- [ ] Verify SEO/meta status classes: lines 11165-11374.
- [ ] Verify status-bar `is-active` branches: lines 11510-11572.
- [ ] Verify chat input classes: lines 11596-11955.
- [ ] Verify budget status classes: lines 12129, 12158, and 12167.
## Phase 11 - Non-Render Class and Selector Audit
- [ ] Preserve WordPress lock key `wpaw-writing`: lines 824 and 828.
- [ ] Preserve body class `wpaw-editor-locked`: lines 825 and 829.
- [ ] Preserve WordPress lock key `wpaw-refining`: lines 835 and 838.
- [ ] Preserve body class `wpaw-refining-locked`: lines 836 and 839.
- [ ] Preserve editor block class `wpaw-block-refining`: lines 895, 903, and 916.
- [ ] Preserve input-blocking selector `.wpaw-sidebar, .wpaw-command-area, .wpaw-messages`: line 933.
- [ ] Preserve diff cleanup removal of `wpaw-diff-removed`: line 5262.
## Phase 12 - Endpoint and Integration Audit
- [ ] Verify `/post-config/${postId}` calls.
- [ ] Verify `/cost-tracking/${postId}` calls.
- [ ] Verify `/writing-state/${postId}` calls.
- [ ] Verify `/seo-audit/${postId}` call.
- [ ] Verify `/generate-meta` call.
- [ ] Verify all conversation endpoints.
- [ ] Verify `/conversation/${postId}` and `/conversation/${data.post_id}` calls.
- [ ] Verify `/chat-history/${postId}` call.
- [ ] Verify `/memanto/restore?post_id=${postId}` call.
- [ ] Verify `/refine-title` call.
- [ ] Verify all `/generate-plan` calls.
- [ ] Verify all `/chat` calls.
- [ ] Verify `/section-blocks/${postId}` and `/section-blocks` calls.
- [ ] Verify `/summarize-context` call.
- [ ] Verify `/detect-intent` call.
- [ ] Verify `/clear-context` call.
- [ ] Verify `/suggest-keywords` call.
- [ ] Verify `/execute-article` call.
- [ ] Verify `/reformat-blocks` call.
- [ ] Verify `/revise-plan` call.
- [ ] Verify `/refine-from-chat` call.
- [ ] Verify `/check-clarity` calls.
- [ ] For every endpoint, verify method, nonce header, content type, body shape, stream/non-stream handling, provider metadata, cost updates, and error handling.
## Phase 13 - Gutenberg and Browser Side Effects Audit
- [ ] Verify all `select("core/block-editor")` calls.
- [ ] Verify all `dispatch("core/block-editor")` calls.
- [ ] Verify all `select("core/editor")` calls.
- [ ] Verify all `dispatch("core/editor")` calls.
- [ ] Verify `wp.data.subscribe` behavior.
- [ ] Verify `wp.data.withSelect` behavior.
- [ ] Verify localStorage keys: `wpaw_agent_workspace_collapsed` and `wpawSessionId_${postId}`.
- [ ] Verify beforeunload handlers.
- [ ] Verify global document event listeners and cleanups.
- [ ] Verify window `wpaw:insert-mention` listener and cleanup.
- [ ] Verify lock heartbeat interval setup and cleanup.
- [ ] Verify debounced config/message/keyword saves.
- [ ] Verify `AbortController` lifecycle.
- [ ] Verify stream reader lifecycle and active reader cancellation.
- [ ] Verify `TextDecoder` usage.
## Phase 14 - Final Parity Audit
- [ ] Confirm no functions are missing from `SIDEBAR_1_TO_1_MIGRATION.md`.
- [ ] Confirm no state/ref/effect entries are missing.
- [ ] Confirm no render surface entries are missing.
- [ ] Confirm no class inventory entries are missing.
- [ ] Confirm no dynamic class expressions were flattened.
- [ ] Confirm no endpoint inventory entries are missing.
- [ ] Confirm no editor/browser side effects are missing.
- [ ] Confirm final plugin registration still renders `ConnectedSidebar`.
- [ ] Confirm no optional extraction was performed before monolith parity.
- [ ] Confirm any future extraction task references the source line range it moves.
## Optional Phase 15 - Extraction After Monolith Parity Only
- [ ] Extract only one helper group at a time.
- [ ] Before extraction, record source range and destination file.
- [ ] Move code without renaming public/internal symbols.
- [ ] Preserve imports/dependencies exactly.
- [ ] Re-run the same parity checks for the moved range.
- [ ] Do not extract render surfaces until all behavior helpers are proven equivalent.
- [ ] Do not delete the monolith source range until the moved range is checked line-by-line.
## Stop Conditions
- [ ] Stop if `sidebar.js` changes and this tasklist has not been regenerated.
- [ ] Stop if a migrated range requires guessing.
- [ ] Stop if a function seems unused but exists in `sidebar.js`.
- [ ] Stop if a class appears styling-only but exists in `sidebar.js`.
- [ ] Stop if an endpoint or request body differs from `sidebar.js`.
- [ ] Stop if hook order would change.

23
TASKLIST_AUDIT_FIXES.md Normal file
View File

@@ -0,0 +1,23 @@
# Audit Fix Tasklist
## Phase 1: UI Theme Consistency & Polish
- [x] 1.1 Make chat messages use dark theme consistently (remove white bg)
- [x] 1.2 Restyle plan cards (remove dashed wireframe look, add fills/icons/status colors)
- [x] 1.3 Fix timeline entry typography (remove monospace, use humanist font)
- [x] 1.4 Structure error messages (icon + title + collapsible detail + action)
- [x] 1.5 Polish input area cohesion (unify focus bar + mode + textarea)
## Phase 2: UX Flow Improvements
- [x] 2.1 Add contextual placeholder text per agent mode in textarea
- [x] 2.2 Add visual mode indicator badge in chat area
- [x] 2.3 Simplify welcome screen (reduce session list noise)
- [x] 2.4 Add slash command discovery hint in empty input
- [x] 2.5 Add confirmation before writing over existing content
- [x] 2.6 Add streaming timeout heartbeat (30s no-data reassurance)
## Phase 3: Error Handling Hardening
- [x] 3.1 Add DB table health check on sidebar init
- [x] 3.2 Improve "no API key" error with settings link
- [x] 3.3 Show in-chat warning when provider fallback triggers
- [x] 3.4 Auto-fallback to registry fallback model on unavailability
- [x] 3.5 Ensure isLoading always resets on all error paths

View File

@@ -18,6 +18,10 @@
color: var(--wpaw-primary);
}
.form-check.mt-3 input[type=checkbox] {
margin-top: .35rem;
}
/* Card enhancements */
.wpaw-settings-v2-wrap .card {
background: transparent !important;

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
models: {},
currentPage: 1,
perPage: 25,
childPerPage: 20,
filters: {
post: '',
model: '',
@@ -475,6 +476,7 @@
if (response.success) {
renderCostLogTable(response.data);
updateCostLogStats(response.data.stats);
renderActionSummary(response.data.stats);
updateFilterOptions(response.data.filters);
renderPagination(response.data);
} else {
@@ -507,6 +509,8 @@
let html = '';
records.forEach((group, index) => {
const collapseId = `collapse-post-${group.post_id}-${index}`;
const detailsTotal = Number(group.details_total || (group.details || []).length || 0);
const detailsInitialEnd = Math.min(state.childPerPage, detailsTotal);
const postCell = group.post_link
? `<a href="${group.post_link}" class="text-decoration-none" target="_blank">${escapeHtml(group.post_title)}</a>`
: `<span class="text-muted">${escapeHtml(group.post_title)}</span>`;
@@ -526,9 +530,13 @@
`;
// Collapsible details row
const detailsHint = detailsTotal > 0
? `<div class="px-3 py-1 small text-muted border-bottom">Showing <span class="wpaw-child-range-start">1</span>-<span class="wpaw-child-range-end">${detailsInitialEnd}</span> of ${detailsTotal} calls</div>`
: '';
html += `
<tr class="collapse wpaw-collapse-row" id="${collapseId}">
<tr class="collapse wpaw-collapse-row" id="${collapseId}" data-child-total="${detailsTotal}">
<td colspan="3" class="p-0">
${detailsHint}
<div class="table-responsive">
<table class="table table-sm mb-0 wpaw-details-table">
<thead>
@@ -541,7 +549,7 @@
<th class="text-end px-3 small text-muted"><?php esc_html_e( 'Cost', 'wp-agentic-writer' ); ?></th>
</tr>
</thead>
<tbody>
<tbody class="wpaw-details-body">
`;
// Detail rows
@@ -562,6 +570,12 @@
</tbody>
</table>
</div>
${detailsTotal > state.childPerPage ? `
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top wpaw-child-pager">
<button type="button" class="button button-small wpaw-child-prev" disabled>Prev</button>
<span class="small text-muted">Page <span class="wpaw-child-page">1</span> of <span class="wpaw-child-pages">${Math.ceil(detailsTotal / state.childPerPage)}</span></span>
<button type="button" class="button button-small wpaw-child-next">Next</button>
</div>` : ''}
</td>
</tr>
`;
@@ -577,15 +591,55 @@
const isExpanded = $(target).hasClass('show');
$icon.toggleClass('dashicons-arrow-right-alt2', !isExpanded);
$icon.toggleClass('dashicons-arrow-down-alt2', isExpanded);
if (isExpanded) {
renderChildPage($(target), 1);
}
}, 10);
});
$(document).off('click.wpawChildPager').on('click.wpawChildPager', '.wpaw-child-prev, .wpaw-child-next', function () {
const $btn = $(this);
const $row = $btn.closest('.wpaw-collapse-row');
const currentPage = Number($row.find('.wpaw-child-page').text() || 1);
const totalPages = Number($row.find('.wpaw-child-pages').text() || 1);
const nextPage = $btn.hasClass('wpaw-child-prev')
? Math.max(1, currentPage - 1)
: Math.min(totalPages, currentPage + 1);
renderChildPage($row, nextPage);
});
// Ensure initial expanded state also starts at page 1 (20 rows),
// not full unpaginated detail rows.
$('.wpaw-collapse-row').each(function () {
renderChildPage($(this), 1);
});
// Update records info
const start = (data.current_page - 1) * data.per_page + 1;
const end = Math.min(data.current_page * data.per_page, data.total_items);
$('#wpaw-records-info').text(`Showing ${start}-${end} of ${data.total_items} posts`);
}
function renderChildPage($row, page) {
const perPage = state.childPerPage || 20;
const $rows = $row.find('.wpaw-details-body tr');
const totalRows = $rows.length;
const totalPages = Math.max(1, Math.ceil(totalRows / perPage));
const safePage = Math.max(1, Math.min(totalPages, page));
const startIdx = (safePage - 1) * perPage;
const endIdx = Math.min(totalRows, startIdx + perPage);
$rows.hide();
$rows.slice(startIdx, endIdx).show();
$row.find('.wpaw-child-page').text(safePage);
$row.find('.wpaw-child-pages').text(totalPages);
$row.find('.wpaw-child-range-start').text(totalRows === 0 ? 0 : startIdx + 1);
$row.find('.wpaw-child-range-end').text(endIdx);
$row.find('.wpaw-child-prev').prop('disabled', safePage <= 1);
$row.find('.wpaw-child-next').prop('disabled', safePage >= totalPages);
}
/**
* Update cost log stats
*/
@@ -597,6 +651,34 @@
$('#wpaw-stat-avg').text('$' + stats.avg_per_post);
}
function renderActionSummary(stats) {
const $tbody = $('#wpaw-action-summary-tbody');
if (!$tbody.length) return;
const rows = Array.isArray(stats?.action_summary) ? stats.action_summary : [];
if (rows.length === 0) {
$tbody.html('<tr><td colspan="4" class="text-center text-muted py-3">No action cost records yet.</td></tr>');
return;
}
const formatAction = (action) => String(action || '')
.replace(/_/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
let html = '';
rows.forEach((row) => {
html += `
<tr>
<td>${escapeHtml(formatAction(row.action))}</td>
<td class="text-end">${Number(row.calls || 0)}</td>
<td class="text-end">$${escapeHtml(String(row.total || '0.0000'))}</td>
<td class="text-end">$${escapeHtml(String(row.average || '0.0000'))}</td>
</tr>
`;
});
$tbody.html(html);
}
/**
* Update filter dropdown options
*/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,421 @@
# OpenRouter BYOK Context and Streaming Spec
**Date:** 2026-06-05
**Status:** Proposed implementation direction
**Goal:** Replace local bash/proxy-first text generation with an OpenRouter BYOK-first API path while preserving article continuity and improving streamed editor UX.
## Decision
Use OpenRouter as the primary text transport for `chat`, `clarity`, `planning`, `writing`, and `refinement`, with the user's OpenRouter workspace configured for BYOK provider keys.
The plugin should continue to store conversation and article memory in WordPress. OpenRouter should be treated as a stateless model gateway: it streams model output, returns usage metadata, applies provider routing, and can cache identical responses. It does not own article continuity.
Local Backend should become optional or legacy. It is useful for experiments, but it should not be the recommended default because it asks users to run local scripts/proxy tooling and creates trust friction.
## Current Implementation Snapshot
The current plugin already has most of the foundation:
- `includes/interface-ai-provider.php` defines `chat()`, `chat_stream()`, `generate_image()`, `is_configured()`, `test_connection()`, and `supports_task_type()`.
- `includes/class-provider-manager.php` routes each task through configured providers and already prevents silent OpenRouter spend when fallback is disabled.
- `includes/class-openrouter-provider.php` supports non-streaming and streaming chat completions through OpenRouter.
- `includes/class-local-backend-provider.php` supports a local proxy at `/v1/messages`, including a cURL streaming parser and plain JSON fallback.
- `includes/class-conversation-manager.php` stores sessions in `{$wpdb->prefix}wpaw_conversations` with `messages` and `context` JSON fields.
- `includes/class-context-service.php` is already documented as the single source of truth for messages, `_wpaw_plan`, `_wpaw_post_config`, and legacy chat migration.
- `includes/class-gutenberg-sidebar.php` exposes the main REST routes: `/chat`, `/generate-plan`, `/revise-plan`, `/execute-article`, `/refine-block`, `/refine-from-chat`, `/summarize-context`, `/detect-intent`, `/writing-state/{post_id}`, and conversation routes.
- Cost tracking already records `post_id`, `session_id`, `model`, `provider`, `action`, input tokens, output tokens, cost, and status.
The main gap is not lack of streaming. The gap is that several routes still accept full `chatHistory` from the browser and inject it into prompts. That makes continuity depend on the browser payload and can re-send too much context.
## Product Positioning
Recommended provider settings:
```php
'task_providers' => array(
'chat' => 'openrouter',
'clarity' => 'openrouter',
'planning' => 'openrouter',
'writing' => 'openrouter',
'refinement' => 'openrouter',
'image' => 'openrouter',
),
'allow_openrouter_fallback' => false,
```
The UI copy should present this as:
- Connect OpenRouter API key.
- Configure BYOK provider keys inside OpenRouter.
- Stream directly into WordPress.
- Keep all article memory in WordPress.
- Local Backend is advanced or legacy.
OpenRouter BYOK details to reflect in docs:
- BYOK lets users route requests through their own provider keys while still using OpenRouter's API surface.
- BYOK provider keys are encrypted and used for requests routed through the selected provider.
- OpenRouter's BYOK fee is documented as 5 percent of the normal OpenRouter model/provider cost, waived for the first 1M BYOK requests per month.
- Users can prevent fallback to OpenRouter shared endpoints by enabling the provider key's "Always use for this provider" behavior in OpenRouter.
- OpenRouter usage data is returned in normal responses and in the last SSE message for streamed responses.
Sources:
- https://openrouter.ai/docs/guides/overview/auth/byok
- https://openrouter.ai/docs/cookbook/administration/usage-accounting
- https://openrouter.ai/docs/guides/features/response-caching/
## Continuity Ownership
Continuity is owned by WordPress, not OpenRouter.
Persisted state:
| State | Current storage | Keep or change |
| --- | --- | --- |
| Conversation messages | `wpaw_conversations.messages` | Keep |
| Session context | `wpaw_conversations.context` | Extend |
| Article plan | `_wpaw_plan` post meta | Keep |
| Post config | `_wpaw_post_config` post meta | Keep |
| Writing state | `_wpaw_writing_status`, `_wpaw_current_section`, `_wpaw_sections_written`, `_wpaw_resume_token` | Keep |
| Section to block mapping | `_wpaw_section_blocks` | Keep |
| Lightweight post memory | `_wpaw_memory` | Extend or migrate into `context` |
| Cost and token usage | `wpaw_cost_tracking` | Extend |
Recommended new session context shape:
```json
{
"working_summary": {
"text": "The article is about ...",
"updated_at": "2026-06-05T10:30:00+07:00",
"source_message_count": 14
},
"decisions": [
{
"type": "accept",
"target": "outline.section.2",
"summary": "Keep the practical checklist framing.",
"created_at": "2026-06-05T10:31:00+07:00"
}
],
"rejections": [
{
"target": "outline.section.4",
"summary": "Too generic; needs concrete WordPress examples.",
"created_at": "2026-06-05T10:32:00+07:00"
}
],
"research_notes": [
{
"source": "manual",
"title": "User supplied constraint",
"excerpt": "Avoid local bash instructions in the default UX.",
"tags": ["trust", "onboarding"]
}
],
"token_policy": {
"max_recent_messages": 6,
"max_summary_tokens": 600,
"max_research_snippets": 5
}
}
```
Store this in `wpaw_conversations.context` first. Avoid adding a new custom table until `context` becomes too large or needs relational querying.
## Context Builder
Add a dedicated builder instead of assembling continuity inside each REST handler.
New file:
```text
includes/class-context-builder.php
```
Primary API:
```php
class WP_Agentic_Writer_Context_Builder {
public function build_for_task( $task, $session_id, $post_id, $request_params = array() ) {
// Returns normalized prompt parts for chat, planning, writing, refinement, SEO.
}
}
```
Return shape:
```php
array(
'system_context' => 'Stable task and policy instructions.',
'working_context' => 'Compact summary, decisions, plan, selected post config.',
'active_content' => 'The exact section/block/article slice being edited.',
'research_context' => 'Only relevant excerpts.',
'audit' => array(
'included_recent_messages' => 6,
'included_research_items' => 3,
'estimated_input_tokens' => 2200,
'used_full_history' => false,
),
)
```
Context assembly rules:
- Always include the task system prompt and language instruction.
- Always include post config summary: audience, tone, language, article length, SEO fields, web search preference.
- Include `_wpaw_plan` for planning, writing, and outline refinement.
- Include only the active block or section for block refinement.
- Include recent raw messages only up to `max_recent_messages`.
- Include `working_summary` when message history is long.
- Include decisions and rejections as compact bullet points.
- Include post content only when the task requires whole-article awareness, such as final polish or article-wide refinement.
- Never trust browser-provided `chatHistory` as authoritative if `sessionId` is available.
## Endpoint Changes
### `/chat`
Current behavior:
- Receives `messages` from the browser.
- Prepends a system prompt.
- Streams or returns a chat response.
- Persists user and assistant messages.
Required change:
- Use browser `messages` only to identify the latest user message.
- Load authoritative session context from `WP_Agentic_Writer_Context_Service`.
- Build final messages through `WP_Agentic_Writer_Context_Builder`.
- Persist the raw user message and assistant response after completion.
### `/generate-plan`
Current behavior:
- Accepts `topic`, `context`, `chatHistory`, and other config.
- Serializes full `chatHistory` into the planning prompt.
- Stores `_wpaw_plan` and `_wpaw_memory`.
Required change:
- Keep `topic`, `context`, `clarificationAnswers`, and `post_config`.
- Replace full `chatHistory` injection with a context package from the builder.
- Save generated plan to `_wpaw_plan`.
- Update `wpaw_conversations.context.working_summary` after plan generation.
### `/revise-plan`
Required behavior:
- Include current `_wpaw_plan`.
- Include latest user instruction.
- Include accepted/rejected outline decisions.
- Ask for raw JSON plan only.
- Save previous plan as a version entry inside `wpaw_conversations.context.plan_versions` before overwriting `_wpaw_plan`.
### `/execute-article`
Current behavior:
- Writes sections from the plan.
- Streams section content and block events.
- Updates `_wpaw_plan` section statuses.
Required change:
- For each section, send the section brief, global article summary, relevant decisions, and relevant research.
- Do not send the full conversation for every section.
- After each section completes, update writing state and append a section summary to session context.
### `/refine-block` and `/refine-from-chat`
Required behavior:
- Send active block content, neighboring heading/section context, relevant plan entry, and latest instruction.
- Include compact working summary and decisions.
- Do not include the full draft unless the requested operation is article-wide.
### `/summarize-context`
Current behavior:
- Summarizes browser-provided `chatHistory`.
- Returns summary but does not appear to be the authoritative persistence mechanism.
Required change:
- Accept `sessionId`.
- Load authoritative session messages.
- Save the resulting summary into `wpaw_conversations.context.working_summary`.
- Return `summary`, `message_count`, `source_message_count`, `tokens_saved`, and provider metadata.
## Streaming Transport
OpenRouter streaming is already implemented in `WP_Agentic_Writer_OpenRouter_Provider::chat_stream()`.
Keep this transport shape:
```php
$body = array(
'model' => $model,
'messages' => $messages,
'stream' => true,
);
```
Modernize usage handling:
- OpenRouter now returns full usage metadata automatically.
- `usage: { include: true }` and `stream_options: { include_usage: true }` are documented as deprecated and no longer required.
- Keep parsing the final `usage` object from streamed chunks.
- Extend cost tracking to store cache metadata when available.
Recommended emitted SSE events:
```json
{"type":"provider","provider":"openrouter","model":"openai/gpt-4o-mini","byok_expected":true}
{"type":"conversational_stream","content":"partial accumulated text"}
{"type":"usage","input_tokens":1200,"output_tokens":360,"cached_tokens":0,"cost":0.0012}
{"type":"complete","session_id":"abc123","totalCost":0.0012}
```
Use the existing browser parsing path in `assets/js/sidebar.js` and add support for the optional `provider` and `usage` event types.
## Response Caching Policy
OpenRouter response caching should be used for deterministic, duplicate-safe operations only. It is not article memory.
Recommended use:
- `detect_intent`
- `summarize_context` retry
- connection test
- repeated model capability lookups if routed through completion calls
Avoid by default:
- article draft generation
- outline revision
- refinement requests
- image prompt generation
Provider implementation change:
```php
if ( ! empty( $options['openrouter_response_cache'] ) ) {
$headers[] = 'X-OpenRouter-Cache: true';
$headers[] = 'X-OpenRouter-Cache-TTL: ' . (int) ( $options['openrouter_cache_ttl'] ?? 300 );
}
```
Important limitations:
- Cache hits only happen for identical requests.
- Streaming and non-streaming requests are cached separately.
- Cache hit usage counters are zeroed.
- Response caching is beta and requires OpenRouter to store response data temporarily.
## Usage and Budget Tracking
Extend `wpaw_cost_tracking` with optional cache and upstream fields:
```sql
ALTER TABLE {$wpdb->prefix}wpaw_cost_tracking
ADD COLUMN cached_tokens int(11) DEFAULT 0 AFTER output_tokens,
ADD COLUMN cache_write_tokens int(11) DEFAULT 0 AFTER cached_tokens,
ADD COLUMN upstream_inference_cost decimal(10,6) DEFAULT NULL AFTER cost,
ADD COLUMN generation_id varchar(64) DEFAULT '' AFTER status;
```
Implementation notes:
- Put this behind a schema version bump, not plugin version alone.
- Keep existing `maybe_upgrade_table()` pattern in `WP_Agentic_Writer_Cost_Tracker`.
- Parse `usage.prompt_tokens_details.cached_tokens`.
- Parse `usage.prompt_tokens_details.cache_write_tokens`.
- Parse `usage.cost_details.upstream_inference_cost` for BYOK requests.
- Include a monthly token budget view alongside the existing cost view.
Budget metric examples:
```php
billable_input_tokens = max( 0, input_tokens - cached_tokens );
total_monthly_tokens = sum( input_tokens + output_tokens );
byok_free_request_counter = count( provider = 'openrouter' and status = 'success' );
```
Note: OpenRouter documents the BYOK waiver as first 1M BYOK requests per month, not first 1M tokens. Keep UI wording precise.
## Settings UI Changes
Update Settings V2:
- Rename default cloud path to `OpenRouter BYOK / API`.
- Keep API key storage in `wp_agentic_writer_settings.openrouter_api_key`.
- Add a help panel explaining that provider BYOK keys are configured in OpenRouter, not in WordPress.
- Add a "Prevent shared fallback" checklist item that links users to OpenRouter BYOK provider settings.
- Move Local Backend to an `Advanced` or `Legacy Local Backend` section.
- Make provider routing default all text tasks to `openrouter`.
- Keep image task on `openrouter`.
- Show a trust note: WordPress streams directly to OpenRouter; no local shell or CLI process is required.
Do not collect provider keys directly in WordPress unless there is a deliberate product decision to bypass OpenRouter BYOK management. The safer default is only storing the OpenRouter API key.
## Migration Plan
### Phase 1: Documentation and defaults
- Add this spec.
- Update user-facing Local Backend docs to say local backend is optional/advanced.
- Default new installs to OpenRouter for all tasks.
- Keep existing installs unchanged unless the user opts in.
### Phase 2: Context builder
- Add `includes/class-context-builder.php`.
- Load it from `wp-agentic-writer.php`.
- Move repeated context assembly out of `class-gutenberg-sidebar.php`.
- Make `/chat`, `/generate-plan`, `/revise-plan`, and refinement endpoints use the builder.
### Phase 3: Authoritative summaries
- Extend `WP_Agentic_Writer_Context_Service` with:
- `get_session_context( $session_id )`
- `update_session_context( $session_id, $patch )`
- `summarize_session_if_needed( $session_id, $post_id )`
- Make `/summarize-context` persist summaries to `wpaw_conversations.context`.
- Store plan versions and section summaries in context.
### Phase 4: Streaming and usage polish
- Remove deprecated OpenRouter usage request parameters.
- Emit optional `provider` and `usage` SSE events.
- Extend cost tracking schema for cached tokens and BYOK upstream cost.
- Add UI display for monthly token usage.
### Phase 5: Local backend repositioning
- Move local backend downloads and setup UI to advanced/legacy.
- Keep `WP_Agentic_Writer_Local_Backend_Provider` for existing users.
- Disable automatic local backend recommendation in onboarding.
## Acceptance Criteria
- A new article can be planned and written through OpenRouter streaming without any local bash/proxy setup.
- Existing conversation history persists through `wpaw_conversations`.
- Plan generation no longer sends full browser `chatHistory` when `sessionId` is available.
- Refining a block includes active block, relevant plan, compact decisions, and recent messages, not full raw history.
- Streaming responses show partial text in the editor and finish with usage metadata.
- Cost tracking records provider, model, action, session, tokens, and cost as it does today.
- New cache fields are recorded when OpenRouter returns them.
- Local Backend still works for users who already configured it, but it is no longer the default recommendation.
## Implementation Risks
- Some existing frontend flows rely on `messages` as the full source of truth. Those flows need to pass `sessionId` reliably before backend context can become authoritative.
- `wpaw_conversations.context` is `LONGTEXT`, so it can hold rich JSON, but large contexts should still be summarized to keep admin queries fast.
- OpenRouter response caching is beta and should not be presented as durable memory.
- BYOK provider fallback behavior is configured in OpenRouter, so the WordPress UI can guide and detect symptoms but cannot fully enforce provider-key policy from this plugin alone.

View File

@@ -0,0 +1,435 @@
# MEMANTO Integration Plan — Optional Context Enhancement
**Version:** 1.0
**Date:** 2026-06-07
**Status:** Planning
**Depends on:** MEMANTO_PRICING_STRATEGY.md
---
## Design Principles
1. **MEMANTO is optional.** The plugin's built-in Context Builder (`class-context-builder.php`) remains the default and always works without MEMANTO.
2. **MEMANTO enhances, never replaces.** MySQL sessions (`wpaw_conversations`) remain the primary session store. MEMANTO runs parallel.
3. **Zero disruption on failure.** If MEMANTO is unreachable, the plugin falls back to existing behavior with no error shown to the user.
4. **Server-side only.** All MEMANTO API calls happen in PHP. The frontend (sidebar.js) is unaware of MEMANTO — it just sees richer or leaner context in AI responses.
5. **User brings own Moorcheh key.** Plugin stores MEMANTO URL + Moorcheh API key in WordPress settings. Never hardcoded.
---
## Architecture Overview
```
┌────────────────────────────────────────────────┐
│ WordPress Backend │
│ │
User Message ──────► │ Gutenberg Sidebar (handle_chat_request) │
│ │ │
│ ▼ │
│ Context Builder (build_system_message) │
│ │ │
│ ├──► MySQL Session (wpaw_conversations) │
│ │ primary store │
│ │ │
│ ├──► Memanto Client ──► MEMANTO API │
│ │ (if configured) │
│ │ │ │
│ │ ├── recall (retrieve) │
│ │ └── remember (store) │
│ │ │
│ ▼ │
│ Merged Context → AI Provider → Response │
└────────────────────────────────────────────────┘
```
### Data Flow: Two Paths
| Path | When | What Happens |
|---|---|---|
| **Default (no MEMANTO)** | MEMANTO URL not configured in settings | Context Builder uses MySQL session only. Identical to current behavior. |
| **MEMANTO active** | MEMANTO URL + Moorcheh key configured and validated | Context Builder queries MEMANTO for relevant memories before building context. After AI response, significant events are stored in MEMANTO. |
---
## Agent Design
### Agent Naming Convention
| Agent ID | Scope | Purpose |
|---|---|---|
| `wp-user-{wordpress_user_id}` | Per WordPress user | Cross-post preferences: writing style, tone, audience, language, brand voice |
| `wp-post-{wordpress_post_id}` | Per post | Article-specific: plan decisions, rejections, research, section progress |
### Memory Types Used
| MEMANTO Type | When Stored | Example |
|---|---|---|
| `preference` | User sets/changes post config | "User prefers conversational tone, intermediate audience" |
| `instruction` | User sends chat message | "Focus on plugin vulnerabilities only" |
| `decision` | User approves/rejects plan | "Approved 5-section outline for WordPress security" |
| `artifact` | Plan generated, section written | "Plan: 5 sections covering X, Y, Z" |
| `context` | Session ends / summarize | "Article at 60% completion, 3 of 5 sections done" |
| `error` | User corrects AI output | "User rejected generic tips approach, wants specific plugin recommendations" |
### Tags Convention
Tags enable targeted recall. Every memory includes:
| Tag | Example | Purpose |
|---|---|---|
| `post:{id}` | `post:42` | Scope recall to specific post |
| `site:{domain}` | `site:example.com` | Scope to WordPress site |
| `mode:{mode}` | `mode:planning` | What mode was active |
| `model:{model}` | `model:deepseek-chat` | Which model was used |
---
## New Files to Create
### `includes/class-memanto-client.php`
PHP client for MEMANTO API v2. Singleton class.
**Public Methods:**
| Method | Description |
|---|---|
| `is_configured()` | Returns true if MEMANTO URL + Moorcheh key are set in settings |
| `is_healthy()` | Calls `/health` endpoint, caches result for 5 minutes |
| `ensure_agent( $agent_id )` | Creates agent via `POST /api/v2/agents` if not exists |
| `activate_session( $agent_id )` | `POST /api/v2/agents/{id}/activate`, caches session token in transient |
| `remember( $agent_id, $content, $type, $tags, $title )` | `POST /api/v2/agents/{id}/remember` |
| `batch_remember( $agent_id, $memories )` | `POST /api/v2/agents/{id}/batch-remember` |
| `recall( $agent_id, $query, $type, $limit )` | `POST /api/v2/agents/{id}/recall` |
| `recall_recent( $agent_id, $limit )` | `POST /api/v2/agents/{id}/recall/recent` |
| `deactivate_session( $agent_id )` | `POST /api/v2/agents/{id}/deactivate` |
**Internal Mechanics:**
- Session token stored in WP transient: `wpaw_memanto_token_{agent_id}` (6-hour TTL matching MEMANTO JWT)
- Auto-reactivates on expired token (catches 401, re-activates, retries)
- All calls use `wp_remote_post` / `wp_remote_get` with 10-second timeout
- All calls wrapped in try/catch with `wpaw_debug_log` on failure
- Moorcheh API key passed via `X-API-Key` header or configured in MEMANTO instance (depending on MEMANTO's auth model)
### `includes/class-memanto-context-enhancer.php`
Orchestrates when and what to remember/recall. Hooks into existing Context Service.
**Public Methods:**
| Method | Hook Point | Description |
|---|---|---|
| `on_session_start( $session_id, $post_id, $user_id )` | Session creation | Ensures user + post agents exist; recalls previous session state |
| `on_user_message( $session_id, $content, $post_id )` | After user sends message | Stores instruction-type memory |
| `on_plan_generated( $post_id, $plan )` | After plan creation | Stores artifact-type memory |
| `on_plan_approved( $post_id, $plan )` | User approves plan | Stores decision-type memory |
| `on_plan_rejected( $post_id, $reason )` | User rejects/requests changes | Stores error-type memory with rejection reason |
| `on_section_written( $post_id, $section_id, $summary )` | After section generation | Stores artifact-type memory |
| `on_block_refined( $post_id, $block_id, $instruction )` | After refinement | Stores instruction-type memory |
| `on_config_saved( $post_id, $config )` | Post config updated | Stores preference-type memory to both user and post agents |
| `on_session_end( $session_id, $post_id )` | Session completed/archived | Summarizes session, stores context-type memory, deactivates session |
| `recall_for_context( $post_id, $user_id, $current_message )` | Before building context | Returns recalled memories to enrich prompt |
**Recall Strategy (`recall_for_context`):**
1. Recall recent memories from post agent (limit: 10)
2. Recall semantically relevant memories from post agent (query: user's current message, limit: 5)
3. Recall user preferences from user agent (query: "writing preferences tone audience", limit: 5)
4. Deduplicate by content hash
5. Return structured array of recalled items
---
## Files to Modify
### `includes/class-settings-v2.php`
Add MEMANTO configuration section:
```
MEMANTO Context Keeper
├── Enable MEMANTO integration (checkbox, default: off)
├── MEMANTO Instance URL (text, e.g., https://abc123.context.wpagentic.dev)
├── Moorcheh API Key (password, user's own key)
└── Connection Status (read-only, shows "Connected" / "Not configured" / "Error: ...")
```
Add a "Test Connection" button that calls MEMANTO `/health` endpoint.
### `includes/class-context-builder.php`
Modify `build_for_task()` method. After line ~52 where `$saved_context` is loaded:
```php
// Existing: MySQL context
$saved_context = $context_service->get_context( $session_id, $post_id );
// NEW: MEMANTO enhancement (if configured)
$memanto_context = array();
$memanto_client = WP_Agentic_Writer_Memanto_Client::get_instance();
if ( $memanto_client->is_configured() && $memanto_client->is_healthy() ) {
$enhancer = WP_Agentic_Writer_Memanto_Context_Enhancer::get_instance();
$memanto_context = $enhancer->recall_for_context(
$post_id,
get_current_user_id(),
$request_params['latestUserMessage'] ?? ''
);
}
```
Modify `build_working_context()` to include a new section:
```php
// After existing sections, before "Recent saved conversation excerpts"
if ( ! empty( $memanto_context ) ) {
$memory_lines = $this->format_memanto_memories( $memanto_context );
if ( '' !== $memory_lines ) {
$sections[] = "PERSISTENT MEMORY (recalled from MEMANTO):\n" . $memory_lines;
}
}
```
**Key rule:** MEMANTO context is **additive**. It never replaces the existing `BACKEND CONTINUITY CONTEXT` section. It supplements it.
### `includes/class-context-service.php`
Add MEMANTO write-through hooks in key methods:
| Method | Hook Added |
|---|---|
| `save_plan()` | `$enhancer->on_plan_generated( $post_id, $plan )` |
| `update_session_context()` | `$enhancer->on_config_saved()` if config changed |
| `add_message()` | `$enhancer->on_user_message()` for user-role messages |
| `clear_context()` | Optionally clear MEMANTO post agent memories |
### `includes/class-gutenberg-sidebar.php`
Add MEMANTO hooks in key handler methods:
| Handler | Hook Added |
|---|---|
| `handle_chat_request()` | `$enhancer->on_user_message()` after saving to MySQL |
| `handle_generate_plan()` | `$enhancer->on_plan_generated()` after successful plan |
| `handle_execute_article()` | `$enhancer->on_section_written()` per section |
| `handle_refine_block()` | `$enhancer->on_block_refined()` |
| `handle_summarize_context()` | Skip AI call if MEMANTO active — return cached recall instead |
| `handle_detect_intent()` | Skip AI call if MEMANTO active — use regex + MEMANTO context instead |
Add new REST endpoint:
```
POST /wp-agentic-writer/v1/memanto/status
→ Returns: { connected: bool, agent_count: int, memory_count: int, last_recall: string }
```
### `includes/class-autoloader.php`
Register the two new classes:
- `class-memanto-client.php``WP_Agentic_Writer_Memanto_Client`
- `class-memanto-context-enhancer.php``WP_Agentic_Writer_Memanto_Context_Enhancer`
### `assets/js/sidebar.js` (minimal change)
No MEMANTO-specific logic needed. Optional enhancement:
- Show a small "🧠 Memory active" indicator when MEMANTO is connected
- Show "memories recalled: N" in the context audit display
---
## Graceful Degradation Strategy
```
MEMANTO call succeeds?
├── YES → Merge MEMANTO context into working context
└── NO
├── MEMANTO not configured → Use MySQL-only context (default behavior)
├── MEMANTO timeout (>10s) → Log warning, use MySQL-only context
├── MEMANTO 401 (token expired) → Re-activate session, retry once, then fallback
└── MEMANTO 5xx (server error) → Log error, use MySQL-only context
```
User **never** sees an error from MEMANTO. The worst case is they get the same experience as users without MEMANTO.
---
## Implementation Phases
### Phase 1: Core Client (Week 1)
**Goal:** MEMANTO client class + settings UI + connection validation
| Task | File | Details |
|---|---|---|
| Create Memanto Client | `class-memanto-client.php` | All API methods, session token management, error handling |
| Create Context Enhancer shell | `class-memanto-context-enhancer.php` | Skeleton with `is_configured()` check on every method |
| Add settings section | `class-settings-v2.php` | URL field, API key field, enable checkbox, test button |
| Register in autoloader | `class-autoloader.php` | Add both new classes |
| Add REST status endpoint | `class-gutenberg-sidebar.php` | `/memanto/status` endpoint |
**Validation:** Admin can configure MEMANTO URL + Moorcheh key, test connection, see "Connected" status. No functional changes to AI features yet.
### Phase 2: Write-Through Memory (Week 2)
**Goal:** Store memories on every meaningful action
| Task | Hook Point | Memory Type |
|---|---|---|
| Store on user message | `handle_chat_request()` | `instruction` |
| Store on plan generated | `handle_generate_plan()` | `artifact` |
| Store on plan approved | After plan save in frontend | `decision` |
| Store on plan rejected | Plan revision flow | `error` |
| Store on section written | `handle_execute_article()` | `artifact` |
| Store on block refined | `handle_refine_block()` | `instruction` |
| Store on config saved | `update_session_context()` | `preference` |
| Store on session end | Session completed/archived | `context` |
**Validation:** Write an article with MEMANTO enabled. Check MEMANTO API (via recall endpoint) that memories were stored. Verify plugin still works perfectly with MEMANTO disabled.
### Phase 3: Context Enrichment (Week 3)
**Goal:** Recall memories to enrich AI prompts
| Task | File | Details |
|---|---|---|
| Add `recall_for_context()` | `class-memanto-context-enhancer.php` | 3-recall strategy (recent, semantic, preferences) |
| Modify `build_for_task()` | `class-context-builder.php` | Merge recalled memories into working context |
| Add `format_memanto_memories()` | `class-context-builder.php` | Format recalled items as compact prompt text |
| Skip summarize-context when MEMANTO active | `class-gutenberg-sidebar.php` | Return cached recall instead of AI call |
| Skip detect-intent when MEMANTO active | `class-gutenberg-sidebar.php` | Use regex + MEMANTO context instead |
**Validation:** Write an article. Mid-session, close the browser. Reopen the post. Verify AI "remembers" context from recalled memories. Compare AI response quality with/without MEMANTO.
### Phase 4: Cross-Session Restore (Week 4)
**Goal:** Seamless experience when returning to a post after days/weeks
| Task | Details |
|---|---|
| Session restore on load | When post editor opens, recall recent post memories. Build a "restored session" system message. |
| Frontend indicator | Show "🧠 Restored from memory" badge in sidebar |
| User preference carry-over | On new post creation, recall user agent preferences for default post config |
| Session deactivation | On session end, call MEMANTO deactivate to trigger summary generation |
**Validation:** Create an article. Complete 50%. Wait 1 day. Open the post again. Verify AI picks up where it left off without user re-explaining context.
### Phase 5: Polish & Edge Cases (Week 5)
| Task | Details |
|---|---|
| Memory pruning | On session end, summarize verbose raw messages into compact context memories |
| Connection health UI | Real-time status indicator in plugin sidebar header |
| Moorcheh limit warning | When approaching 10K vectors, show admin notice with upgrade link |
| Error logging | Detailed MEMANTO error logging with `wpaw_debug_log` |
| Settings validation | Validate URL format, API key format, connection test before saving |
---
## Testing Strategy
### Unit Tests
| Test | Description |
|---|---|
| `test_memanto_client_not_configured` | Client returns false when settings empty |
| `test_memanto_client_health_check` | Mock `/health` response, verify caching |
| `test_memanto_client_remember` | Mock remember API, verify payload structure |
| `test_memanto_client_recall` | Mock recall API, verify response parsing |
| `test_memanto_client_session_lifecycle` | Activate → remember → recall → deactivate |
| `test_enhancer_graceful_fallback` | MEMANTO returns error, context builder still works |
| `test_context_builder_with_memanto` | Verify MEMANTO context is included in working context |
| `test_context_builder_without_memanto` | Verify no MEMANTO content when not configured |
### Integration Tests
| Test | Description |
|---|---|
| Full article with MEMANTO | Chat → Plan → Write → Refine. Verify memories stored at each step. |
| Full article without MEMANTO | Same flow. Verify no MEMANTO calls made. Plugin works identically. |
| MEMANTO goes down mid-session | Start with MEMANTO active. Simulate timeout. Verify graceful fallback. |
| Cross-session restore | Write 50% of article. Simulate new session. Verify AI context restored. |
| Multi-site with same MEMANTO | Use same MEMANTO instance across 2 sites. Verify agent isolation. |
### Manual Test Checklist
- [ ] Plugin activates with no MEMANTO settings — works normally
- [ ] MEMANTO URL set but Moorcheh key empty — shows "not configured"
- [ ] MEMANTO URL + invalid key — shows "connection error"
- [ ] MEMANTO URL + valid key — shows "connected"
- [ ] Write article with MEMANTO on — AI responses include recalled memory
- [ ] Write article with MEMANTO off — identical to current behavior
- [ ] Disable MEMANTO mid-session — no errors, fallback to MySQL-only
- [ ] Re-enable MEMANTO — picks up from where it left off
- [ ] Check MEMANTO recall endpoint — memories exist for test post
---
## Performance Considerations
| Concern | Mitigation |
|---|---|
| MEMANTO recall adds latency to every AI call | Cache recall results in transient (5-min TTL). Only recall when context builder runs. |
| Session token expires mid-request | Auto-reactivate on 401. Single retry. |
| Too many memories stored | Batch-remember to reduce HTTP calls. Summarize on session end. |
| MEMANTO instance overloaded | 10-second timeout on all calls. Graceful fallback. |
| WordPress transient cache bloat | Use specific key patterns. Clean up on session end. |
---
## Settings UI Specification
### MEMANTO Context Keeper Section
Located in WP Agentic Writer → Settings → MEMANTO tab.
```
┌─────────────────────────────────────────────────────────────────┐
│ MEMANTO Context Keeper │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ☑ Enable MEMANTO integration │
│ │
│ MEMANTO Instance URL │
│ ┌──────────────────────────────────────────────────┐ │
│ │ https://abc123.context.wpagentic.dev │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Moorcheh API Key │
│ ┌──────────────────────────────────────────────────┐ │
│ │ •••••••••••••••••••••••• │ │
│ └──────────────────────────────────────────────────┘ │
Get a free API key at moorcheh.ai (10K vectors/month) │
│ │
│ Connection Status: 🟢 Connected │
│ Last checked: 2 minutes ago │
│ │
│ [Test Connection] │
│ │
├─────────────────────────────────────────────────────────────────┤
MEMANTO is an optional add-on that provides persistent │
│ memory for your AI writing assistant. Your AI will remember │
│ context across sessions and posts. The plugin works │
│ perfectly without MEMANTO. │
│ │
│ Get MEMANTO at: wpagentic.dev/memanto │
└─────────────────────────────────────────────────────────────────┘
```
---
## Summary
| Aspect | Decision |
|---|---|
| **Scope** | Optional enhancement, not a dependency |
| **New files** | `class-memanto-client.php`, `class-memanto-context-enhancer.php` |
| **Modified files** | `class-context-builder.php`, `class-context-service.php`, `class-gutenberg-sidebar.php`, `class-settings-v2.php`, `class-autoloader.php` |
| **Frontend changes** | Minimal: status indicator only |
| **Fallback behavior** | Full graceful degradation to MySQL-only context |
| **Implementation time** | 5 weeks (1 week per phase) |
| **Testing priority** | Phase 2 (write-through) and Phase 3 (recall) are critical paths |
---
**Document Date:** June 7, 2026
**Status:** Draft — Ready for Phase 1 implementation

View File

@@ -1,7 +0,0 @@
# Ignore node_modules in local backend package
agentic-writer-local-backend/node_modules/
agentic-writer-local-backend/proxy.log
agentic-writer-local-backend/proxy.pid
# Keep the distributable ZIP
!agentic-writer-local-backend.zip

View File

@@ -1,170 +0,0 @@
# Agentic Writer Local Backend
Run unlimited AI content generation on your own machine using your Claude CLI + Z.ai/Anthropic account.
## Prerequisites
Before starting, ensure you have:
-**Claude CLI** installed and configured
- Get it: [https://claude.ai/code](https://claude.ai/code) or [https://z.ai](https://z.ai)
- Verify: `claude --version` or `which claude`
-**Node.js 18+** installed
- Download: [https://nodejs.org](https://nodejs.org)
- Verify: `node --version`
-**Z.ai Coding Plan** or **Anthropic API key** configured in Claude CLI
## Quick Start
### 1. Extract Package
```bash
unzip agentic-writer-local-backend.zip
cd agentic-writer-local-backend
```
### 2. Start the Proxy
```bash
chmod +x *.sh
./start-proxy.sh
```
You'll see:
```
═══════════════════════════════════════════════════
✅ Local Backend Running!
═══════════════════════════════════════════════════
Your Configuration:
Base URL: http://192.168.1.105:8080
API Key: dummy
Model: claude-local
```
### 3. Configure WordPress Plugin
1. Open **WP Admin****Agentic Writer****Settings****Local Backend**
2. Paste the **Base URL** shown above
3. API Key: `dummy`
4. Click **Test Connection** → should show ✅
5. Start generating content!
## Commands
```bash
./start-proxy.sh # Start proxy (runs in background)
./stop-proxy.sh # Stop proxy
./test-connection.sh # Test if proxy responds
./get-local-ip.sh # Find your local IP address
tail -f proxy.log # View real-time logs
```
## Firewall Setup
The proxy needs to accept connections from your WordPress site.
### macOS
1. **System Settings****Network****Firewall**
2. Click **Options****Add** → Select `node`
3. Set to **Allow incoming connections**
### Linux (ufw)
```bash
sudo ufw allow 8080/tcp
sudo ufw reload
```
### Windows
1. **Windows Defender Firewall****Advanced Settings**
2. **Inbound Rules****New Rule**
3. **Port** → TCP **8080****Allow**
## How It Works
```
WordPress Plugin → HTTP POST → Local Proxy (port 8080)
Spawns Claude CLI
Returns AI Response
```
**Benefits:**
- 🆓 **Free**: Uses your existing Z.ai/Anthropic subscription
- 🔒 **Private**: Content never leaves your network
-**Fast**: LAN latency (~50-200ms)
- 🚀 **Unlimited**: No rate limits, no token counting
## Troubleshooting
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed solutions.
### Quick Fixes
**"Connection failed" in plugin:**
```bash
# Check proxy is running
ps aux | grep claude-proxy
# Restart if needed
./stop-proxy.sh && ./start-proxy.sh
```
**"Claude CLI not found":**
```bash
# Verify Claude is installed
which claude
claude --version
# Test Claude works
echo "Hello" | claude
```
**"Wrong IP address":**
```bash
# Find your correct IP
./get-local-ip.sh
# Or manually:
# macOS: ipconfig getifaddr en0
# Linux: ip route get 1 | awk '{print $7}'
```
**Port 8080 already in use:**
```bash
# Find what's using it
lsof -i :8080
# Change port (edit claude-proxy.js)
PORT=9000 node claude-proxy.js
# Update plugin Base URL to: http://your-ip:9000
```
## Security Notes
- Proxy binds to `0.0.0.0` (all network interfaces) for LAN access
- No authentication by design (LAN trust model)
- All request prompts are logged to `proxy.log`
- For internet exposure, use ngrok/reverse proxy with authentication
## Environment Variables
```bash
PORT=9000 ./start-proxy.sh # Use different port
NODE_ENV=production # Production mode
```
## Support
- **Documentation**: [Plugin Docs](https://github.com/your/plugin)
- **Issues**: [GitHub Issues](https://github.com/your/plugin/issues)
- **Community**: [Discord](https://discord.gg/your-server)
## License
GPL-2.0+ - Same as WP Agentic Writer plugin

View File

@@ -1,339 +0,0 @@
# Troubleshooting Guide
Common issues and solutions for Agentic Writer Local Backend.
## Connection Issues
### "Connection timeout" in Plugin
**Symptoms:**
- Plugin shows "Connection timeout" error
- Test connection fails
**Solutions:**
1. **Check proxy is running:**
```bash
ps aux | grep claude-proxy
```
2. **Restart proxy:**
```bash
./stop-proxy.sh
./start-proxy.sh
```
3. **Check logs:**
```bash
tail -f proxy.log
```
4. **Verify IP address:**
```bash
./get-local-ip.sh
```
### "Connection refused"
**Cause:** Proxy not running or wrong IP
**Solutions:**
1. **Start proxy:**
```bash
./start-proxy.sh
```
2. **Check firewall:**
- macOS: System Settings → Network → Firewall → Allow Node.js
- Linux: `sudo ufw allow 8080/tcp`
- Windows: Defender Firewall → Allow port 8080
3. **Test locally first:**
```bash
curl http://localhost:8080/ping
# Should return: pong
```
## Claude CLI Issues
### "Claude CLI not found"
**Verify installation:**
```bash
which claude
# macOS: /opt/homebrew/bin/claude or /usr/local/bin/claude
# Linux: ~/.local/bin/claude or /usr/bin/claude
```
**Fix PATH:**
```bash
# Add to ~/.zshrc or ~/.bashrc
export PATH="/opt/homebrew/bin:$PATH"
source ~/.zshrc
```
**Reinstall Claude CLI:**
- Visit: [https://claude.ai/code](https://claude.ai/code)
- Follow installation instructions
### "No response from Claude"
**Test Claude manually:**
```bash
echo "Hello, reply with: Test successful" | claude
```
**Check authentication:**
```bash
claude --version
# Should show version and auth status
```
**Reconfigure Claude:**
- Check Z.ai account: [https://z.ai](https://z.ai)
- Or Anthropic API key setup
## Network Issues
### Wrong IP Address Detected
**Find correct IP:**
```bash
# macOS
ipconfig getifaddr en0 # WiFi
ipconfig getifaddr en1 # Ethernet
# Linux
ip route get 1 | awk '{print $7}'
hostname -I
# Windows
ipconfig
# Look for "IPv4 Address" under active adapter
```
**Update plugin settings:**
- Use the correct IP in Base URL: `http://CORRECT-IP:8080`
### Port 8080 Already in Use
**Find what's using it:**
```bash
lsof -i :8080
# or
netstat -anp | grep 8080
```
**Change port:**
1. Edit `claude-proxy.js`:
```javascript
const PORT = process.env.PORT || 9000; // Change 8080 to 9000
```
2. Restart proxy:
```bash
./stop-proxy.sh
PORT=9000 ./start-proxy.sh
```
3. Update plugin Base URL: `http://your-ip:9000`
## Performance Issues
### Slow Response Times
**Normal latency:**
- Local network: 50-200ms
- Claude CLI processing: 2-30 seconds depending on prompt
**If consistently slow:**
1. **Check network:**
```bash
ping 192.168.1.105 # Your proxy IP
```
2. **Monitor logs:**
```bash
tail -f proxy.log
```
3. **Check machine resources:**
- CPU usage: Claude CLI is CPU-intensive
- Memory: Ensure sufficient RAM available
### Proxy Crashes
**Check logs:**
```bash
cat proxy.log | tail -50
```
**Common causes:**
- Out of memory: Close other applications
- Claude CLI timeout: Increase timeout in `claude-proxy.js`
- Malformed requests: Check plugin version compatibility
**Restart with clean state:**
```bash
./stop-proxy.sh
rm proxy.log
./start-proxy.sh
```
## Plugin Integration Issues
### "Invalid response format"
**Cause:** Claude response doesn't match expected JSON format
**Debug:**
1. Check `proxy.log` for actual Claude output
2. Test manually:
```bash
curl -X POST http://localhost:8080/v1/messages \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Hello"}]}'
```
3. Update Claude CLI if outdated:
```bash
claude --version
# Upgrade if needed
```
### Cost Tracking Shows $0
**Expected behavior:** Local backend is free, plugin should show `$0.00 (Local)`
**If concerned:**
- This is correct - local backend has no API costs
- Dashboard should show "X requests local (free)"
## Advanced Troubleshooting
### Enable Debug Logging
Edit `claude-proxy.js`:
```javascript
const DEBUG = true; // Add at top of file
// In /v1/messages handler:
if (DEBUG) {
console.log('Full request:', JSON.stringify(req.body, null, 2));
console.log('Full response:', output);
}
```
### Test with curl
**Ping:**
```bash
curl http://localhost:8080/ping
# Expected: pong
```
**Inference:**
```bash
curl -X POST http://localhost:8080/v1/messages \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "Reply with: Test successful"}
]
}'
```
**Expected response:**
```json
{
"id": "local-1234567890",
"object": "chat.completion",
"model": "claude-local",
"choices": [{
"message": {
"content": "Test successful"
}
}]
}
```
### Permissions Issues (macOS)
**Make scripts executable:**
```bash
chmod +x start-proxy.sh stop-proxy.sh test-connection.sh get-local-ip.sh
```
**If "permission denied":**
```bash
# Check file permissions
ls -la *.sh
# Reset if needed
chmod 755 *.sh
```
## Still Having Issues?
1. **Check system requirements:**
- Node.js 18+: `node --version`
- Claude CLI installed: `which claude`
- Sufficient disk space: `df -h`
2. **Collect diagnostic info:**
```bash
echo "Node version:" $(node --version)
echo "Claude path:" $(which claude)
echo "Local IP:" $(./get-local-ip.sh)
echo "Proxy status:" $(ps aux | grep claude-proxy)
tail -20 proxy.log
```
3. **Reset everything:**
```bash
./stop-proxy.sh
rm -rf node_modules proxy.log proxy.pid
npm install
./start-proxy.sh
```
4. **Get help:**
- GitHub Issues: [Report Bug](https://github.com/your/plugin/issues)
- Discord Community: [Join Chat](https://discord.gg/your-server)
- Include: OS, Node version, Claude CLI version, error logs
## Environment-Specific Notes
### macOS
- Default Claude path: `/opt/homebrew/bin/claude`
- Firewall: System Settings → Network → Firewall
- IP detection: `ipconfig getifaddr en0`
### Linux
- Default Claude path: `~/.local/bin/claude`
- Firewall: `sudo ufw allow 8080/tcp`
- IP detection: `ip route get 1 | awk '{print $7}'`
### Windows
- Claude path varies, check `where claude`
- Firewall: Windows Defender → Allow port 8080
- IP detection: `ipconfig` (look for IPv4)
- Scripts: Use Git Bash or WSL to run `.sh` scripts
## Security Best Practices
1. **LAN only:** Don't expose proxy to internet without authentication
2. **Firewall:** Restrict to specific IPs if on shared network
3. **Logs:** `proxy.log` contains all prompts - review periodically
4. **Updates:** Keep Node.js and Claude CLI updated
---
**Last Updated:** 2025-02-27
**Version:** 1.0.0

View File

@@ -1,122 +0,0 @@
const express = require('express');
const { spawn } = require('child_process');
const app = express();
app.use(express.json());
// Health check endpoint
app.get('/ping', (req, res) => {
res.send('pong');
});
// Main inference endpoint (OpenAI-compatible format)
app.post('/v1/messages', async (req, res) => {
const { messages } = req.body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({
error: {
message: 'Invalid request: messages array required'
}
});
}
// Extract the last user message as the prompt
const lastMessage = messages[messages.length - 1];
const prompt = lastMessage.content;
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('Request from:', req.ip);
console.log('Prompt length:', prompt.length, 'chars');
console.log('Prompt preview:', prompt.substring(0, 150) + '...');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
// Spawn Claude CLI process
const claude = spawn('claude', [], {
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
claude.stdout.on('data', (data) => {
output += data.toString();
process.stdout.write('.');
});
claude.stderr.on('data', (data) => {
errorOutput += data.toString();
console.error('Claude stderr:', data.toString());
});
claude.on('close', (code) => {
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('Claude exit code:', code);
console.log('Response length:', output.length, 'chars');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
if (code !== 0 || !output.trim()) {
return res.status(500).json({
error: {
message: 'Claude CLI error',
details: errorOutput || 'No response from Claude'
}
});
}
// Return OpenAI-compatible response format
res.json({
id: 'local-' + Date.now(),
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: 'claude-local',
choices: [{
index: 0,
message: {
role: 'assistant',
content: output.trim()
},
finish_reason: 'stop'
}],
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
});
});
claude.on('error', (err) => {
console.error('Failed to spawn Claude CLI:', err);
res.status(500).json({
error: {
message: 'Failed to spawn Claude CLI',
details: err.message
}
});
});
// Send prompt to Claude after brief pause
setTimeout(() => {
claude.stdin.write(prompt + '\n');
claude.stdin.end();
}, 100);
});
const PORT = process.env.PORT || 8080;
app.listen(PORT, '0.0.0.0', () => {
console.log('═══════════════════════════════════════════════════');
console.log('🚀 Agentic Writer Local Backend Started!');
console.log('═══════════════════════════════════════════════════');
console.log(`Local: http://localhost:${PORT}`);
console.log(`Network: http://YOUR-IP:${PORT}`);
console.log('');
console.log('Plugin Configuration:');
console.log(` Base URL: http://YOUR-IP:${PORT}`);
console.log(` API Key: dummy`);
console.log(` Model: claude-local`);
console.log('');
console.log('Health check: GET /ping');
console.log('Inference: POST /v1/messages');
console.log('═══════════════════════════════════════════════════');
});

View File

@@ -1,34 +0,0 @@
#!/bin/bash
echo "Detecting your local IP address..."
echo ""
# Detect local IP based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS - try en0 (WiFi) then en1 (Ethernet)
IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "")
INTERFACE=$(ifconfig en0 &>/dev/null && echo "en0 (WiFi)" || echo "en1 (Ethernet)")
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || hostname -I | awk '{print $1}')
INTERFACE="default"
else
# Windows or unknown
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
INTERFACE="unknown"
fi
if [ -z "$IP" ]; then
echo "❌ Could not detect IP address automatically"
echo ""
echo "Manual detection:"
echo " macOS: ipconfig getifaddr en0"
echo " Linux: ip route get 1 | awk '{print \$7}'"
echo " Windows: ipconfig (look for IPv4 Address)"
exit 1
fi
echo "✅ Your local IP: $IP ($INTERFACE)"
echo ""
echo "Use this in your plugin settings:"
echo " Base URL: http://$IP:8080"

View File

@@ -1,21 +0,0 @@
{
"name": "agentic-writer-local-backend",
"version": "1.0.0",
"description": "Local backend proxy for WP Agentic Writer using Claude CLI",
"main": "claude-proxy.js",
"scripts": {
"start": "node claude-proxy.js",
"test": "curl http://localhost:8080/ping"
},
"keywords": [
"wordpress",
"ai",
"claude",
"proxy"
],
"author": "WP Agentic Writer",
"license": "GPL-2.0+",
"dependencies": {
"express": "^4.18.2"
}
}

View File

@@ -1,77 +0,0 @@
#!/bin/bash
echo "🚀 Starting Agentic Writer Local Backend..."
echo ""
# Check dependencies
if ! command -v node &> /dev/null; then
echo "❌ Node.js not found. Install from https://nodejs.org/"
exit 1
fi
if ! command -v claude &> /dev/null; then
echo "❌ Claude CLI not found. Install and configure first."
echo " Check: https://claude.ai/code or https://z.ai"
exit 1
fi
# Auto-install express if needed
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
# Detect local IP
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "127.0.0.1")
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
LOCAL_IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || echo "127.0.0.1")
else
# Windows/other
LOCAL_IP="127.0.0.1"
fi
echo "✅ Dependencies OK"
echo "✅ Claude CLI found: $(which claude)"
echo ""
echo "Starting proxy server..."
echo ""
# Start server in background
nohup node claude-proxy.js > proxy.log 2>&1 &
PID=$!
echo $PID > proxy.pid
# Wait for server to boot
sleep 2
# Test if running
if kill -0 $PID 2>/dev/null; then
echo "═══════════════════════════════════════════════════"
echo "✅ Local Backend Running!"
echo "═══════════════════════════════════════════════════"
echo ""
echo "Your Configuration:"
echo " Base URL: http://$LOCAL_IP:8080"
echo " API Key: dummy"
echo " Model: claude-local"
echo ""
echo "Next Steps:"
echo " 1. Open your WordPress Admin"
echo " 2. Go to Agentic Writer → Settings → Local Backend"
echo " 3. Paste the Base URL above"
echo " 4. Click 'Test Connection'"
echo ""
echo "Commands:"
echo " Logs: tail -f proxy.log"
echo " Stop: ./stop-proxy.sh"
echo " Test: ./test-connection.sh"
echo "═══════════════════════════════════════════════════"
else
echo "❌ Failed to start. Check proxy.log for errors."
cat proxy.log
rm -f proxy.pid
exit 1
fi

View File

@@ -1,21 +0,0 @@
#!/bin/bash
if [ -f proxy.pid ]; then
PID=$(cat proxy.pid)
if kill -0 $PID 2>/dev/null; then
kill $PID
rm proxy.pid
echo "🛑 Local Backend stopped (PID: $PID)"
else
echo "⚠️ No process found with PID: $PID"
rm proxy.pid
fi
else
# Fallback: kill by process name
pkill -f claude-proxy.js
if [ $? -eq 0 ]; then
echo "🛑 Stopped all claude-proxy processes"
else
echo " No claude-proxy processes running"
fi
fi

View File

@@ -1,42 +0,0 @@
#!/bin/bash
echo "Testing local backend connection..."
echo ""
# Test /ping endpoint
echo "1. Testing health check..."
PING_RESPONSE=$(curl -s http://localhost:8080/ping 2>&1)
if [ "$PING_RESPONSE" = "pong" ]; then
echo " ✅ Health check passed"
else
echo " ❌ Health check failed"
echo " Response: $PING_RESPONSE"
echo ""
echo "Is the proxy running? Check: ps aux | grep claude-proxy"
exit 1
fi
# Test /v1/messages endpoint
echo "2. Testing inference..."
RESPONSE=$(curl -s -X POST http://localhost:8080/v1/messages \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Reply with exactly: Test successful"}]}' 2>&1)
echo " Response: $RESPONSE"
if echo "$RESPONSE" | grep -q "choices"; then
echo " ✅ Inference endpoint working"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Local Backend is working correctly!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
echo " ❌ Inference test failed"
echo ""
echo "Troubleshooting:"
echo " 1. Check Claude CLI: echo 'test' | claude"
echo " 2. Check logs: tail -f proxy.log"
echo " 3. Restart proxy: ./stop-proxy.sh && ./start-proxy.sh"
exit 1
fi

View File

@@ -1,217 +0,0 @@
# Agentic Writer Local Backend
Run unlimited AI content generation on your own machine using your Claude CLI + Z.ai/Anthropic account.
## Prerequisites
Before starting, ensure you have:
-**Claude CLI** installed and configured
- Get it: [https://claude.ai/code](https://claude.ai/code) or [https://z.ai](https://z.ai)
- Verify: `claude --version` or `which claude`
-**Node.js 18+** installed
- Download: [https://nodejs.org](https://nodejs.org)
- Verify: `node --version`
-**Z.ai Coding Plan** or **Anthropic API key** configured in Claude CLI
## Quick Start
### 1. Extract Package
```bash
unzip agentic-writer-local-backend.zip
cd agentic-writer-local-backend
```
### 2. Start the Proxy
```bash
chmod +x *.sh
./start-proxy.sh
```
You'll see:
```
═══════════════════════════════════════════════════
✅ Local Backend Running!
═══════════════════════════════════════════════════
Your Configuration:
Base URL: http://192.168.1.105:8080
API Key: dummy
Model: claude-local
```
### 3. Configure WordPress Plugin
1. Open **WP Admin****Agentic Writer****Settings****Local Backend**
2. Paste the **Base URL** shown above
3. API Key: `dummy`
4. Click **Test Connection** → should show ✅
5. Start generating content!
## Commands
```bash
./start-proxy.sh # Start proxy (runs in background)
./stop-proxy.sh # Stop proxy
./test-connection.sh # Test if proxy responds
./get-local-ip.sh # Find your local IP address
tail -f proxy.log # View real-time logs
```
## Firewall Setup
The proxy needs to accept connections from your WordPress site.
### macOS
1. **System Settings****Network****Firewall**
2. Click **Options****Add** → Select `node`
3. Set to **Allow incoming connections**
### Linux (ufw)
```bash
sudo ufw allow 8080/tcp
sudo ufw reload
```
### Windows
1. **Windows Defender Firewall****Advanced Settings**
2. **Inbound Rules****New Rule**
3. **Port** → TCP **8080****Allow**
## How It Works
```
WordPress Plugin → HTTP POST → Local Proxy (port 8080)
Spawns Claude CLI
Returns AI Response
```
**Benefits:**
- 🆓 **Free**: Uses your existing Z.ai/Anthropic subscription
- 🔒 **Private**: Content never leaves your network
-**Fast**: LAN latency (~50-200ms)
- 🚀 **Unlimited**: No rate limits, no token counting
## Troubleshooting
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed solutions.
### Quick Fixes
**"Connection failed" in plugin:**
```bash
# Check proxy is running
ps aux | grep claude-proxy
# Restart if needed
./stop-proxy.sh && ./start-proxy.sh
```
**"Claude CLI not found":**
```bash
# Verify Claude is installed
which claude
claude --version
# Test Claude works
echo "Hello" | claude
```
**"Wrong IP address":**
```bash
# Find your correct IP
./get-local-ip.sh
# Or manually:
# macOS: ipconfig getifaddr en0
# Linux: ip route get 1 | awk '{print $7}'
```
**Port 8080 already in use:**
```bash
# Find what's using it
lsof -i :8080
# Change port (edit claude-proxy.js)
PORT=9000 node claude-proxy.js
# Update plugin Base URL to: http://your-ip:9000
```
## Security Notes
- Proxy binds to `0.0.0.0` (all network interfaces) for LAN access
- No authentication by design (LAN trust model)
- All request prompts are logged to `proxy.log`
- For internet exposure, use ngrok/reverse proxy with authentication
## Environment Variables
```bash
# Use different port (default: 8080)
PORT=9000 ./start-proxy.sh
# Production mode
NODE_ENV=production
# Brave Search API (for web search capability)
export BRAVE_SEARCH_API_KEY="your-brave-api-key"
```
### Enabling Web Search (Brave Search)
To enable web search in your AI responses:
1. **Get a Brave Search API key** from [https://brave.com/search/api/](https://brave.com/search/api/)
2. **Configure it in one of these ways:**
**Option 1: Add to `.env` file (recommended for this proxy)**
```bash
echo 'BRAVE_SEARCH_API_KEY="BSA03Yj-your-key-here"' > .env
```
**Option 2: Add to Claude Code settings**
Add to `~/.claude/settings.json`:
```json
{
"env": {
"BRAVE_SEARCH_API_KEY": "your-key-here"
}
}
```
**Option 3: Add to shell profile**
```bash
export BRAVE_SEARCH_API_KEY="your-key-here"
```
3. **Restart the proxy**:
```bash
./stop-proxy.sh && ./start-proxy.sh
```
When the proxy starts, you should see:
```
Brave Search:
API Key: CONFIGURED
```
**Note:** Web search must also be enabled in the WordPress plugin settings (Agentic Writer → Settings → General → Search → Enable). The plugin will automatically use search results when planning or researching topics.
## Support
- **Documentation**: [Plugin Docs](https://github.com/your/plugin)
- **Issues**: [GitHub Issues](https://github.com/your/plugin/issues)
- **Community**: [Discord](https://discord.gg/your-server)
## License
GPL-2.0+ - Same as WP Agentic Writer plugin

View File

@@ -1,339 +0,0 @@
# Troubleshooting Guide
Common issues and solutions for Agentic Writer Local Backend.
## Connection Issues
### "Connection timeout" in Plugin
**Symptoms:**
- Plugin shows "Connection timeout" error
- Test connection fails
**Solutions:**
1. **Check proxy is running:**
```bash
ps aux | grep claude-proxy
```
2. **Restart proxy:**
```bash
./stop-proxy.sh
./start-proxy.sh
```
3. **Check logs:**
```bash
tail -f proxy.log
```
4. **Verify IP address:**
```bash
./get-local-ip.sh
```
### "Connection refused"
**Cause:** Proxy not running or wrong IP
**Solutions:**
1. **Start proxy:**
```bash
./start-proxy.sh
```
2. **Check firewall:**
- macOS: System Settings → Network → Firewall → Allow Node.js
- Linux: `sudo ufw allow 8080/tcp`
- Windows: Defender Firewall → Allow port 8080
3. **Test locally first:**
```bash
curl http://localhost:8080/ping
# Should return: pong
```
## Claude CLI Issues
### "Claude CLI not found"
**Verify installation:**
```bash
which claude
# macOS: /opt/homebrew/bin/claude or /usr/local/bin/claude
# Linux: ~/.local/bin/claude or /usr/bin/claude
```
**Fix PATH:**
```bash
# Add to ~/.zshrc or ~/.bashrc
export PATH="/opt/homebrew/bin:$PATH"
source ~/.zshrc
```
**Reinstall Claude CLI:**
- Visit: [https://claude.ai/code](https://claude.ai/code)
- Follow installation instructions
### "No response from Claude"
**Test Claude manually:**
```bash
echo "Hello, reply with: Test successful" | claude
```
**Check authentication:**
```bash
claude --version
# Should show version and auth status
```
**Reconfigure Claude:**
- Check Z.ai account: [https://z.ai](https://z.ai)
- Or Anthropic API key setup
## Network Issues
### Wrong IP Address Detected
**Find correct IP:**
```bash
# macOS
ipconfig getifaddr en0 # WiFi
ipconfig getifaddr en1 # Ethernet
# Linux
ip route get 1 | awk '{print $7}'
hostname -I
# Windows
ipconfig
# Look for "IPv4 Address" under active adapter
```
**Update plugin settings:**
- Use the correct IP in Base URL: `http://CORRECT-IP:8080`
### Port 8080 Already in Use
**Find what's using it:**
```bash
lsof -i :8080
# or
netstat -anp | grep 8080
```
**Change port:**
1. Edit `claude-proxy.js`:
```javascript
const PORT = process.env.PORT || 9000; // Change 8080 to 9000
```
2. Restart proxy:
```bash
./stop-proxy.sh
PORT=9000 ./start-proxy.sh
```
3. Update plugin Base URL: `http://your-ip:9000`
## Performance Issues
### Slow Response Times
**Normal latency:**
- Local network: 50-200ms
- Claude CLI processing: 2-30 seconds depending on prompt
**If consistently slow:**
1. **Check network:**
```bash
ping 192.168.1.105 # Your proxy IP
```
2. **Monitor logs:**
```bash
tail -f proxy.log
```
3. **Check machine resources:**
- CPU usage: Claude CLI is CPU-intensive
- Memory: Ensure sufficient RAM available
### Proxy Crashes
**Check logs:**
```bash
cat proxy.log | tail -50
```
**Common causes:**
- Out of memory: Close other applications
- Claude CLI timeout: Increase timeout in `claude-proxy.js`
- Malformed requests: Check plugin version compatibility
**Restart with clean state:**
```bash
./stop-proxy.sh
rm proxy.log
./start-proxy.sh
```
## Plugin Integration Issues
### "Invalid response format"
**Cause:** Claude response doesn't match expected JSON format
**Debug:**
1. Check `proxy.log` for actual Claude output
2. Test manually:
```bash
curl -X POST http://localhost:8080/v1/messages \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Hello"}]}'
```
3. Update Claude CLI if outdated:
```bash
claude --version
# Upgrade if needed
```
### Cost Tracking Shows $0
**Expected behavior:** Local backend is free, plugin should show `$0.00 (Local)`
**If concerned:**
- This is correct - local backend has no API costs
- Dashboard should show "X requests local (free)"
## Advanced Troubleshooting
### Enable Debug Logging
Edit `claude-proxy.js`:
```javascript
const DEBUG = true; // Add at top of file
// In /v1/messages handler:
if (DEBUG) {
console.log('Full request:', JSON.stringify(req.body, null, 2));
console.log('Full response:', output);
}
```
### Test with curl
**Ping:**
```bash
curl http://localhost:8080/ping
# Expected: pong
```
**Inference:**
```bash
curl -X POST http://localhost:8080/v1/messages \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "Reply with: Test successful"}
]
}'
```
**Expected response:**
```json
{
"id": "local-1234567890",
"object": "chat.completion",
"model": "claude-local",
"choices": [{
"message": {
"content": "Test successful"
}
}]
}
```
### Permissions Issues (macOS)
**Make scripts executable:**
```bash
chmod +x start-proxy.sh stop-proxy.sh test-connection.sh get-local-ip.sh
```
**If "permission denied":**
```bash
# Check file permissions
ls -la *.sh
# Reset if needed
chmod 755 *.sh
```
## Still Having Issues?
1. **Check system requirements:**
- Node.js 18+: `node --version`
- Claude CLI installed: `which claude`
- Sufficient disk space: `df -h`
2. **Collect diagnostic info:**
```bash
echo "Node version:" $(node --version)
echo "Claude path:" $(which claude)
echo "Local IP:" $(./get-local-ip.sh)
echo "Proxy status:" $(ps aux | grep claude-proxy)
tail -20 proxy.log
```
3. **Reset everything:**
```bash
./stop-proxy.sh
rm -rf node_modules proxy.log proxy.pid
npm install
./start-proxy.sh
```
4. **Get help:**
- GitHub Issues: [Report Bug](https://github.com/your/plugin/issues)
- Discord Community: [Join Chat](https://discord.gg/your-server)
- Include: OS, Node version, Claude CLI version, error logs
## Environment-Specific Notes
### macOS
- Default Claude path: `/opt/homebrew/bin/claude`
- Firewall: System Settings → Network → Firewall
- IP detection: `ipconfig getifaddr en0`
### Linux
- Default Claude path: `~/.local/bin/claude`
- Firewall: `sudo ufw allow 8080/tcp`
- IP detection: `ip route get 1 | awk '{print $7}'`
### Windows
- Claude path varies, check `where claude`
- Firewall: Windows Defender → Allow port 8080
- IP detection: `ipconfig` (look for IPv4)
- Scripts: Use Git Bash or WSL to run `.sh` scripts
## Security Best Practices
1. **LAN only:** Don't expose proxy to internet without authentication
2. **Firewall:** Restrict to specific IPs if on shared network
3. **Logs:** `proxy.log` contains all prompts - review periodically
4. **Updates:** Keep Node.js and Claude CLI updated
---
**Last Updated:** 2025-02-27
**Version:** 1.0.0

View File

@@ -1,279 +0,0 @@
const express = require('express');
const { spawn } = require('child_process');
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.json());
// Try multiple sources for Brave API Key (in order of priority):
// 1. Environment variable
// 2. .env file in proxy directory
// 3. ~/.claude/settings.json (Claude Code config)
function getBraveApiKey() {
// 1. Check environment variable first
if (process.env.BRAVE_SEARCH_API_KEY) {
return process.env.BRAVE_SEARCH_API_KEY;
}
// 2. Check .env file in proxy directory
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
const match = envContent.match(/BRAVE_SEARCH_API_KEY\s*=\s*(.+)/m);
if (match) {
return match[1].trim();
}
}
// 3. Check Claude Code settings.json
const claudeSettingsPath = path.join(process.env.HOME || '/root', '.claude', 'settings.json');
if (fs.existsSync(claudeSettingsPath)) {
try {
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
if (settings.env?.BRAVE_SEARCH_API_KEY) {
return settings.env.BRAVE_SEARCH_API_KEY;
}
} catch (e) {
// Ignore JSON parse errors
}
}
return '';
}
// Health check endpoint
app.get('/ping', (req, res) => {
const status = {
status: 'pong',
braveSearchConfigured: !!BRAVE_API_KEY,
timestamp: new Date().toISOString()
};
res.json(status);
});
// Main inference endpoint (OpenAI-compatible format)
app.post('/v1/messages', async (req, res) => {
const { messages, stream } = req.body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({
error: {
message: 'Invalid request: messages array required'
}
});
}
// Check if web search is requested (via X-Search-Enabled header)
const webSearchEnabled = req.headers['x-search-enabled'] === 'true';
const searchQuery = req.headers['x-search-query'] || '';
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('Request from:', req.ip);
console.log('Web Search:', webSearchEnabled ? 'ENABLED' : 'disabled');
if (searchQuery) {
console.log('Search Query:', searchQuery.substring(0, 100) + '...');
}
const braveApiKey = getBraveApiKey();
console.log('Brave API Key:', braveApiKey ? 'CONFIGURED' : 'NOT SET');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
// If web search is enabled and we have a query, fetch search results first
let searchContext = '';
if (webSearchEnabled && searchQuery && braveApiKey) {
console.log('Fetching web search results...');
try {
searchContext = await fetchBraveSearchResults(searchQuery, braveApiKey);
console.log('Search results fetched:', searchContext.length, 'chars');
} catch (err) {
console.error('Search error:', err.message);
}
}
// Build conversation context from messages array
// Include previous messages for context continuity
let conversationPrompt = '';
for (const msg of messages) {
const role = msg.role === 'assistant' ? 'Assistant' : 'User';
conversationPrompt += `${role}: ${msg.content}\n\n`;
}
let prompt = conversationPrompt.trim();
// Prepend search context if available
if (searchContext) {
prompt = `WEB SEARCH RESULTS:\n${searchContext}\n\n---\n\nUSER QUERY:\n${prompt}\n\nPlease answer based on the search results above when relevant.`;
}
console.log('Prompt length:', prompt.length, 'chars');
console.log('Prompt preview:', prompt.substring(0, 150) + '...');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
// Spawn Claude CLI process
const claude = spawn('claude', [], {
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
claude.stdout.on('data', (data) => {
output += data.toString();
process.stdout.write('.');
});
claude.stderr.on('data', (data) => {
errorOutput += data.toString();
console.error('Claude stderr:', data.toString());
});
claude.on('close', (code) => {
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('Claude exit code:', code);
console.log('Response length:', output.length, 'chars');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
if (code !== 0 || !output.trim()) {
return res.status(500).json({
error: {
message: 'Claude CLI error',
details: errorOutput || 'No response from Claude'
}
});
}
// Return OpenAI-compatible response format
res.json({
id: 'local-' + Date.now(),
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: 'claude-local',
choices: [{
index: 0,
message: {
role: 'assistant',
content: output.trim()
},
finish_reason: 'stop'
}],
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
});
});
claude.on('error', (err) => {
console.error('Failed to spawn Claude CLI:', err);
res.status(500).json({
error: {
message: 'Failed to spawn Claude CLI',
details: err.message
}
});
});
// Send prompt to Claude after brief pause
setTimeout(() => {
claude.stdin.write(prompt + '\n');
claude.stdin.end();
}, 100);
});
/**
* Fetch search results from Brave Search API
*/
async function fetchBraveSearchResults(query, apiKey, count = 5) {
return new Promise((resolve, reject) => {
const encodedQuery = encodeURIComponent(query);
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}&count=${count}`;
const options = {
headers: {
'Accept': 'application/json',
'X-Subscription-Token': apiKey
}
};
const protocol = url.startsWith('https') ? https : http;
const request = protocol.get(url, options, (response) => {
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
if (response.statusCode !== 200) {
return reject(new Error(`Brave API error: ${response.statusCode}`));
}
try {
const json = JSON.parse(data);
const results = json.web?.results || [];
if (results.length === 0) {
return resolve('No search results found.');
}
// Format results for LLM consumption
let formatted = 'Search Results:\n\n';
results.forEach((result, i) => {
formatted += `${i + 1}. **${result.title}**\n`;
formatted += ` URL: ${result.url}\n`;
if (result.description) {
formatted += ` Summary: ${result.description}\n`;
}
formatted += '\n';
});
resolve(formatted);
} catch (err) {
reject(new Error('Failed to parse Brave response'));
}
});
});
request.on('error', (err) => {
reject(err);
});
request.setTimeout(10000, () => {
request.destroy();
reject(new Error('Brave search timeout'));
});
});
}
const PORT = process.env.PORT || 8080;
app.listen(PORT, '0.0.0.0', () => {
const braveApiKey = getBraveApiKey();
console.log('═══════════════════════════════════════════════════');
console.log('🚀 Agentic Writer Local Backend v1.1.0');
console.log('═══════════════════════════════════════════════════');
console.log(`Local: http://localhost:${PORT}`);
console.log(`Network: http://YOUR-IP:${PORT}`);
console.log('');
console.log('Plugin Configuration:');
console.log(` Base URL: http://YOUR-IP:${PORT}`);
console.log(` API Key: dummy`);
console.log(` Model: claude-local`);
console.log('');
console.log('Brave Search:');
console.log(` API Key: ${braveApiKey ? 'CONFIGURED' : 'NOT SET'}`);
console.log('');
console.log('Web search works when Brave API key is found from:');
console.log(' 1. Environment: export BRAVE_SEARCH_API_KEY="key"');
console.log(' 2. .env file: BRAVE_SEARCH_API_KEY=key');
console.log(' 3. ~/.claude/settings.json env.BRAVE_SEARCH_API_KEY');
console.log('');
console.log('Restart proxy after adding key: ./stop-proxy.sh && ./start-proxy.sh');
console.log('');
console.log('Health check: GET /ping');
console.log('Inference: POST /v1/messages');
console.log('═══════════════════════════════════════════════════');
});

View File

@@ -1,34 +0,0 @@
#!/bin/bash
echo "Detecting your local IP address..."
echo ""
# Detect local IP based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS - try en0 (WiFi) then en1 (Ethernet)
IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "")
INTERFACE=$(ifconfig en0 &>/dev/null && echo "en0 (WiFi)" || echo "en1 (Ethernet)")
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || hostname -I | awk '{print $1}')
INTERFACE="default"
else
# Windows or unknown
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
INTERFACE="unknown"
fi
if [ -z "$IP" ]; then
echo "❌ Could not detect IP address automatically"
echo ""
echo "Manual detection:"
echo " macOS: ipconfig getifaddr en0"
echo " Linux: ip route get 1 | awk '{print \$7}'"
echo " Windows: ipconfig (look for IPv4 Address)"
exit 1
fi
echo "✅ Your local IP: $IP ($INTERFACE)"
echo ""
echo "Use this in your plugin settings:"
echo " Base URL: http://$IP:8080"

View File

@@ -1,828 +0,0 @@
{
"name": "agentic-writer-local-backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agentic-writer-local-backend",
"version": "1.0.0",
"license": "GPL-2.0+",
"dependencies": {
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -1,21 +0,0 @@
{
"name": "agentic-writer-local-backend",
"version": "1.0.0",
"description": "Local backend proxy for WP Agentic Writer using Claude CLI",
"main": "claude-proxy.js",
"scripts": {
"start": "node claude-proxy.js",
"test": "curl http://localhost:8080/ping"
},
"keywords": [
"wordpress",
"ai",
"claude",
"proxy"
],
"author": "WP Agentic Writer",
"license": "GPL-2.0+",
"dependencies": {
"express": "^4.18.2"
}
}

View File

@@ -1,92 +0,0 @@
#!/bin/bash
echo "🚀 Starting Agentic Writer Local Backend..."
echo ""
# Check dependencies
if ! command -v node &> /dev/null; then
echo "❌ Node.js not found. Install from https://nodejs.org/"
exit 1
fi
if ! command -v claude &> /dev/null; then
echo "❌ Claude CLI not found. Install and configure first."
echo " Check: https://claude.ai/code or https://z.ai"
exit 1
fi
# Auto-install express if needed
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
# Load environment variables from .env file if it exists
if [ -f .env ]; then
echo "📋 Loading environment from .env file..."
set -a # automatically export all variables created
source .env
set +a # stop auto-export
fi
# Detect local IP
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "127.0.0.1")
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
LOCAL_IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || echo "127.0.0.1")
else
# Windows/other
LOCAL_IP="127.0.0.1"
fi
echo "✅ Dependencies OK"
echo "✅ Claude CLI found: $(which claude)"
echo ""
echo "Starting proxy server..."
echo ""
# Start server in background
nohup node claude-proxy.js > proxy.log 2>&1 &
PID=$!
echo $PID > proxy.pid
# Wait for server to boot
sleep 2
# Test if running
if kill -0 $PID 2>/dev/null; then
echo "═══════════════════════════════════════════════════"
echo "✅ Local Backend Running!"
echo "═══════════════════════════════════════════════════"
echo ""
echo "Your Configuration:"
echo " Base URL: http://$LOCAL_IP:8080"
echo " API Key: dummy"
echo " Model: claude-local"
echo ""
if [ -n "$BRAVE_SEARCH_API_KEY" ]; then
echo "Brave Search: ✅ CONFIGURED"
else
echo "Brave Search: ⚠️ NOT SET (web search disabled)"
echo " To enable: Add BRAVE_SEARCH_API_KEY to .env file"
fi
echo ""
echo "Next Steps:"
echo " 1. Open your WordPress Admin"
echo " 2. Go to Agentic Writer → Settings → Local Backend"
echo " 3. Paste the Base URL above"
echo " 4. Click 'Test Connection'"
echo ""
echo "Commands:"
echo " Logs: tail -f proxy.log"
echo " Stop: ./stop-proxy.sh"
echo " Test: ./test-connection.sh"
echo "═══════════════════════════════════════════════════"
else
echo "❌ Failed to start. Check proxy.log for errors."
cat proxy.log
rm -f proxy.pid
exit 1
fi

View File

@@ -1,21 +0,0 @@
#!/bin/bash
if [ -f proxy.pid ]; then
PID=$(cat proxy.pid)
if kill -0 $PID 2>/dev/null; then
kill $PID
rm proxy.pid
echo "🛑 Local Backend stopped (PID: $PID)"
else
echo "⚠️ No process found with PID: $PID"
rm proxy.pid
fi
else
# Fallback: kill by process name
pkill -f claude-proxy.js
if [ $? -eq 0 ]; then
echo "🛑 Stopped all claude-proxy processes"
else
echo " No claude-proxy processes running"
fi
fi

View File

@@ -1,42 +0,0 @@
#!/bin/bash
echo "Testing local backend connection..."
echo ""
# Test /ping endpoint
echo "1. Testing health check..."
PING_RESPONSE=$(curl -s http://localhost:8080/ping 2>&1)
if [ "$PING_RESPONSE" = "pong" ]; then
echo " ✅ Health check passed"
else
echo " ❌ Health check failed"
echo " Response: $PING_RESPONSE"
echo ""
echo "Is the proxy running? Check: ps aux | grep claude-proxy"
exit 1
fi
# Test /v1/messages endpoint
echo "2. Testing inference..."
RESPONSE=$(curl -s -X POST http://localhost:8080/v1/messages \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Reply with exactly: Test successful"}]}' 2>&1)
echo " Response: $RESPONSE"
if echo "$RESPONSE" | grep -q "choices"; then
echo " ✅ Inference endpoint working"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Local Backend is working correctly!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
echo " ❌ Inference test failed"
echo ""
echo "Troubleshooting:"
echo " 1. Check Claude CLI: echo 'test' | claude"
echo " 2. Check logs: tail -f proxy.log"
echo " 3. Restart proxy: ./stop-proxy.sh && ./start-proxy.sh"
exit 1
fi

View File

@@ -0,0 +1,744 @@
<?php
/**
* Context Builder
*
* Builds compact, backend-owned prompt context from saved sessions and posts.
*
* @package WP_Agentic_Writer
*/
if (!defined("ABSPATH")) {
exit();
}
/**
* Class WP_Agentic_Writer_Context_Builder
*
* OpenRouter and other providers are stateless gateways. This class keeps
* continuity in WordPress by selecting the relevant session/post context for
* each request without resending the full browser history.
*/
class WP_Agentic_Writer_Context_Builder
{
/**
* Singleton instance.
*
* @var WP_Agentic_Writer_Context_Builder
*/
private static $instance = null;
/**
* Get singleton instance.
*
* @return WP_Agentic_Writer_Context_Builder
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Build a context package for a task.
*
* @param string $task Task name.
* @param string $session_id Session ID.
* @param int $post_id Post ID.
* @param array $request_params Request params.
* @return array Context package.
*/
public function build_for_task(
$task,
$session_id,
$post_id,
$request_params = [],
) {
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
$saved_context = !empty($session_id)
? $context_service->get_context($session_id, $post_id)
: [];
$session_context = $saved_context["context"] ?? [];
$messages = $saved_context["messages"] ?? [];
if (empty($messages)) {
$messages = $this->get_request_messages($request_params);
}
$token_policy = $this->get_token_policy($session_context);
$recent_messages = $this->prepare_recent_messages(
$messages,
$token_policy["max_recent_messages"],
);
$recent_messages = $this->remove_active_user_message(
$recent_messages,
$request_params["latestUserMessage"] ?? "",
);
$post_config = $this->resolve_post_config(
$saved_context,
$request_params,
);
$plan = $this->resolve_plan($saved_context, $request_params, $post_id);
$working_context = $this->build_working_context(
$task,
$session_context,
$recent_messages,
$plan,
$post_config,
$request_params,
$post_id,
);
// Check MEMANTO status for audit (lightweight: just checks is_active, no recall).
$memanto_active = WP_Agentic_Writer_Memanto_Client::get_instance()->is_active();
return [
"system_context" => "",
"working_context" => $working_context,
"active_content" => $this->get_active_content($request_params),
"research_context" => $this->build_research_context(
$session_context,
$request_params,
$token_policy["max_research_snippets"],
),
"audit" => [
"included_recent_messages" => count($recent_messages),
"included_research_items" => $this->count_research_items(
$session_context,
$request_params,
$token_policy["max_research_snippets"],
),
"estimated_input_tokens" => $this->estimate_tokens(
$working_context,
),
"used_full_history" => false,
"memanto_active" => $memanto_active,
],
];
}
/**
* Build a system message that can be inserted after the primary system prompt.
*
* @param string $task Task name.
* @param string $session_id Session ID.
* @param int $post_id Post ID.
* @param array $request_params Request params.
* @return array Context system message and audit metadata.
*/
public function build_system_message(
$task,
$session_id,
$post_id,
$request_params = [],
) {
$package = $this->build_for_task(
$task,
$session_id,
$post_id,
$request_params,
);
$content = trim(
$package["working_context"] .
"\n" .
$package["active_content"] .
"\n" .
$package["research_context"],
);
if ("" === $content) {
return [
"message" => null,
"audit" => $package["audit"],
];
}
return [
"message" => [
"role" => "system",
"content" => $content,
],
"audit" => $package["audit"],
];
}
/**
* Get task token policy.
*
* @param array $session_context Session context.
* @return array Token policy.
*/
private function get_token_policy($session_context)
{
$policy =
isset($session_context["token_policy"]) &&
is_array($session_context["token_policy"])
? $session_context["token_policy"]
: [];
return [
"max_recent_messages" => max(
2,
(int) ($policy["max_recent_messages"] ?? 6),
),
"max_summary_tokens" => max(
200,
(int) ($policy["max_summary_tokens"] ?? 600),
),
"max_research_snippets" => max(
0,
(int) ($policy["max_research_snippets"] ?? 5),
),
];
}
/**
* Get messages from request fallback.
*
* @param array $request_params Request params.
* @return array Messages.
*/
private function get_request_messages($request_params)
{
if (
!empty($request_params["messages"]) &&
is_array($request_params["messages"])
) {
return $request_params["messages"];
}
if (
!empty($request_params["chatHistory"]) &&
is_array($request_params["chatHistory"])
) {
return $request_params["chatHistory"];
}
return [];
}
/**
* Prepare compact recent messages.
*
* @param array $messages Messages.
* @param int $max_messages Max messages.
* @return array Recent messages.
*/
private function prepare_recent_messages($messages, $max_messages)
{
$prepared = [];
foreach ((array) $messages as $message) {
$role = isset($message["role"]) ? (string) $message["role"] : "";
if (!in_array($role, ["user", "assistant"], true)) {
continue;
}
$content = isset($message["content"])
? trim(wp_strip_all_tags((string) $message["content"]))
: "";
if ("" === $content) {
continue;
}
$prepared[] = [
"role" => $role,
"content" => $this->truncate_text($content, 900),
];
}
if (count($prepared) > $max_messages) {
$prepared = array_slice($prepared, -1 * $max_messages);
}
return $prepared;
}
/**
* Avoid echoing the active user turn inside the saved-context excerpt.
*
* @param array $messages Recent messages.
* @param string $active_user_message Active user message.
* @return array Messages without duplicate active turn.
*/
private function remove_active_user_message($messages, $active_user_message)
{
$active_user_message = trim(
wp_strip_all_tags((string) $active_user_message),
);
if ("" === $active_user_message || empty($messages)) {
return $messages;
}
for ($i = count($messages) - 1; $i >= 0; $i--) {
if ("user" !== ($messages[$i]["role"] ?? "")) {
continue;
}
$content = trim((string) ($messages[$i]["content"] ?? ""));
if ($content === $active_user_message) {
array_splice($messages, $i, 1);
}
break;
}
return $messages;
}
/**
* Resolve post config.
*
* @param array $saved_context Saved context package.
* @param array $request_params Request params.
* @return array Post config.
*/
private function resolve_post_config($saved_context, $request_params)
{
$config = $saved_context["post_config"] ?? [];
if (
!empty($request_params["postConfig"]) &&
is_array($request_params["postConfig"])
) {
$config = wp_parse_args($request_params["postConfig"], $config);
}
return is_array($config) ? $config : [];
}
/**
* Resolve current plan.
*
* @param array $saved_context Saved context package.
* @param array $request_params Request params.
* @param int $post_id Post ID.
* @return array|null Plan.
*/
private function resolve_plan($saved_context, $request_params, $post_id)
{
if (
!empty($saved_context["plan"]) &&
is_array($saved_context["plan"])
) {
return $saved_context["plan"];
}
if (
!empty($request_params["plan"]) &&
is_array($request_params["plan"])
) {
return $request_params["plan"];
}
if ($post_id > 0) {
$plan = get_post_meta($post_id, "_wpaw_plan", true);
if (is_array($plan)) {
return $plan;
}
}
return null;
}
/**
* Build compact working context.
*
* @param string $task Task name.
* @param array $session_context Session context.
* @param array $recent_messages Recent messages.
* @param array $plan Current plan.
* @param array $post_config Post config.
* @param array $request_params Request params.
* @param int $post_id Post ID.
* @return string Context text.
*/
private function build_working_context(
$task,
$session_context,
$recent_messages,
$plan,
$post_config,
$request_params,
$post_id = 0,
) {
$sections = [];
$sections[] =
"BACKEND CONTINUITY CONTEXT\nUse this compact WordPress-saved context as continuity. Do not assume OpenRouter remembers prior turns.";
$sections[] = "Current task: " . sanitize_key($task);
// MEMANTO persistent memory injection.
$memanto_context = $this->build_memanto_context(
$post_id,
$request_params["latestUserMessage"] ?? "",
);
if ("" !== $memanto_context) {
$sections[] = $memanto_context;
}
$summary = $session_context["working_summary"]["text"] ?? "";
if ("" !== trim((string) $summary)) {
$sections[] =
"Working summary:\n" .
$this->truncate_text((string) $summary, 1600);
}
$config_summary = $this->summarize_post_config($post_config);
if ("" !== $config_summary) {
$sections[] = "Article configuration:\n" . $config_summary;
}
$plan_summary = $this->summarize_plan($plan);
if ("" !== $plan_summary) {
$sections[] = "Current plan:\n" . $plan_summary;
}
$decision_summary = $this->summarize_context_items(
$session_context,
"decisions",
"Decisions",
);
if ("" !== $decision_summary) {
$sections[] = $decision_summary;
}
$rejection_summary = $this->summarize_context_items(
$session_context,
"rejections",
"Rejected directions",
);
if ("" !== $rejection_summary) {
$sections[] = $rejection_summary;
}
if (!empty($request_params["context"])) {
$sections[] =
"User supplied context:\n" .
$this->truncate_text((string) $request_params["context"], 1600);
}
if (!empty($recent_messages)) {
$lines = [];
foreach ($recent_messages as $message) {
$lines[] =
ucfirst($message["role"]) . ": " . $message["content"];
}
$sections[] =
"Recent saved conversation excerpts:\n" . implode("\n", $lines);
}
return implode("\n\n", array_filter($sections));
}
/**
* Summarize post config.
*
* @param array $post_config Post config.
* @return string Summary.
*/
private function summarize_post_config($post_config)
{
$lines = [];
$keys = [
"article_length" => "Article length",
"language" => "Language",
"tone" => "Tone",
"audience" => "Audience",
"experience_level" => "Experience level",
"seo_focus_keyword" => "SEO focus keyword",
"seo_secondary_keywords" => "SEO secondary keywords",
];
foreach ($keys as $key => $label) {
if (
isset($post_config[$key]) &&
"" !== trim((string) $post_config[$key])
) {
$lines[] =
"- " . $label . ": " . trim((string) $post_config[$key]);
}
}
if (isset($post_config["include_images"])) {
$lines[] =
"- Include images: " .
($post_config["include_images"] ? "yes" : "no");
}
if (isset($post_config["web_search"])) {
$lines[] =
"- Web search: " . ($post_config["web_search"] ? "yes" : "no");
}
return implode("\n", $lines);
}
/**
* Summarize current plan.
*
* @param array|null $plan Plan.
* @return string Summary.
*/
private function summarize_plan($plan)
{
if (empty($plan) || !is_array($plan)) {
return "";
}
$lines = [];
if (!empty($plan["title"])) {
$lines[] = "Title: " . $plan["title"];
}
if (!empty($plan["sections"]) && is_array($plan["sections"])) {
foreach ($plan["sections"] as $index => $section) {
$heading = $section["heading"] ?? ($section["title"] ?? "");
if ("" === trim((string) $heading)) {
continue;
}
$status = $section["status"] ?? "pending";
$lines[] = sprintf(
"%d. [%s] %s",
$index + 1,
$status,
$heading,
);
}
}
return implode("\n", $lines);
}
/**
* Summarize context array items.
*
* @param array $session_context Session context.
* @param string $key Context key.
* @param string $label Label.
* @return string Summary.
*/
private function summarize_context_items($session_context, $key, $label)
{
if (
empty($session_context[$key]) ||
!is_array($session_context[$key])
) {
return "";
}
$lines = [];
foreach (array_slice($session_context[$key], -8) as $item) {
$summary = $item["summary"] ?? "";
if ("" === trim((string) $summary)) {
continue;
}
$target = !empty($item["target"])
? "[" . $item["target"] . "] "
: "";
$lines[] = "- " . $target . $summary;
}
return empty($lines) ? "" : $label . ":\n" . implode("\n", $lines);
}
/**
* Get active content from request params.
*
* @param array $request_params Request params.
* @return string Active content context.
*/
private function get_active_content($request_params)
{
$candidates = [
"activeContent",
"blockContent",
"selectedText",
"sectionContent",
"articleContent",
];
$lines = [];
foreach ($candidates as $key) {
if (
!empty($request_params[$key]) &&
is_string($request_params[$key])
) {
$lines[] =
$key .
":\n" .
$this->truncate_text($request_params[$key], 2200);
}
}
return empty($lines)
? ""
: "ACTIVE CONTENT SLICE\n" . implode("\n\n", $lines);
}
/**
* Build research context.
*
* @param array $session_context Session context.
* @param array $request_params Request params.
* @param int $limit Max snippets.
* @return string Research context.
*/
private function build_research_context(
$session_context,
$request_params,
$limit,
) {
if ($limit <= 0) {
return "";
}
$items = [];
if (
!empty($session_context["research_notes"]) &&
is_array($session_context["research_notes"])
) {
$items = array_merge($items, $session_context["research_notes"]);
}
if (
!empty($request_params["researchNotes"]) &&
is_array($request_params["researchNotes"])
) {
$items = array_merge($items, $request_params["researchNotes"]);
}
$items = array_slice($items, -1 * $limit);
$lines = [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$title = $item["title"] ?? ($item["source"] ?? "Research note");
$excerpt = $item["excerpt"] ?? ($item["notes"] ?? "");
if ("" === trim((string) $excerpt)) {
continue;
}
$lines[] =
"- " .
$title .
": " .
$this->truncate_text((string) $excerpt, 700);
}
return empty($lines)
? ""
: "RELEVANT RESEARCH\n" . implode("\n", $lines);
}
/**
* Count included research items.
*
* @param array $session_context Session context.
* @param array $request_params Request params.
* @param int $limit Max snippets.
* @return int Count.
*/
private function count_research_items(
$session_context,
$request_params,
$limit,
) {
if ($limit <= 0) {
return 0;
}
$count = 0;
if (
!empty($session_context["research_notes"]) &&
is_array($session_context["research_notes"])
) {
$count += count($session_context["research_notes"]);
}
if (
!empty($request_params["researchNotes"]) &&
is_array($request_params["researchNotes"])
) {
$count += count($request_params["researchNotes"]);
}
return min($limit, $count);
}
/**
* Build MEMANTO persistent memory context section.
*
* Calls recall_for_context() via the MEMANTO Context Enhancer
* and formats the returned memories into a prompt-ready section.
* Returns empty string when MEMANTO is inactive or no memories found.
*
* @param int $post_id Post ID.
* @param string $current_message User's current message for semantic search.
* @return string Formatted memory section or empty string.
*/
private function build_memanto_context($post_id, $current_message = "")
{
// Guard: skip entirely if MEMANTO is not active.
$memanto = WP_Agentic_Writer_Memanto_Context_Enhancer::get_instance();
$memories = $memanto->recall_for_context(
$post_id,
get_current_user_id(),
$current_message,
);
if (empty($memories) || !is_array($memories)) {
return "";
}
$lines = [];
foreach ($memories as $memory) {
$type = ucfirst($memory["type"] ?? "memory");
$content = trim((string) ($memory["content"] ?? ""));
if ("" === $content) {
continue;
}
$title = !empty($memory["title"])
? " [" . trim($memory["title"]) . "]"
: "";
$lines[] = "- ({$type}){$title} {$content}";
}
if (empty($lines)) {
return "";
}
return "PERSISTENT MEMORY (from MEMANTO)\n" .
"The following are memories recalled from prior sessions and interactions. " .
"Use them to maintain continuity, respect user preferences, and avoid repeating past mistakes.\n" .
implode("\n", $lines);
}
/**
* Truncate text safely.
*
* @param string $text Text.
* @param int $limit Character limit.
* @return string Truncated text.
*/
private function truncate_text($text, $limit)
{
$text = trim((string) $text);
if (strlen($text) <= $limit) {
return $text;
}
return substr($text, 0, $limit) . "...";
}
/**
* Estimate tokens from character length.
*
* @param string $text Text.
* @return int Estimated tokens.
*/
private function estimate_tokens($text)
{
return (int) ceil(strlen((string) $text) / 4);
}
}

View File

@@ -177,6 +177,78 @@ class WP_Agentic_Writer_Context_Service {
return $manager->update_messages( $session_id, $messages );
}
/**
* Get structured session context JSON.
*
* @since 0.2.3
* @param string $session_id Session ID.
* @return array Session context.
*/
public function get_session_context( $session_id ) {
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
$session = $manager->get_session( $session_id );
if ( ! $session || empty( $session['context'] ) || ! is_array( $session['context'] ) ) {
return array();
}
return $session['context'];
}
/**
* Merge a context patch into the stored session context.
*
* @since 0.2.3
* @param string $session_id Session ID.
* @param array $patch Context fields to merge.
* @return bool Success.
*/
public function update_session_context( $session_id, $patch ) {
if ( empty( $session_id ) || ! is_array( $patch ) ) {
return false;
}
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
$context = $this->get_session_context( $session_id );
$context = $this->merge_context_recursive( $context, $patch );
$context['updated_at'] = current_time( 'c' );
return $manager->update_context( $session_id, $context );
}
/**
* Append an item to an array inside session context.
*
* @since 0.2.3
* @param string $session_id Session ID.
* @param string $key Context key.
* @param array $item Item to append.
* @param int $limit Maximum retained items.
* @return bool Success.
*/
public function append_session_context_item( $session_id, $key, $item, $limit = 25 ) {
if ( empty( $session_id ) || empty( $key ) || ! is_array( $item ) ) {
return false;
}
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
$context = $this->get_session_context( $session_id );
if ( empty( $context[ $key ] ) || ! is_array( $context[ $key ] ) ) {
$context[ $key ] = array();
}
$item['created_at'] = $item['created_at'] ?? current_time( 'c' );
$context[ $key ][] = $item;
if ( $limit > 0 && count( $context[ $key ] ) > $limit ) {
$context[ $key ] = array_slice( $context[ $key ], -1 * $limit );
}
$context['updated_at'] = current_time( 'c' );
return $manager->update_context( $session_id, $context );
}
/**
* Save plan to post meta.
*
@@ -254,6 +326,7 @@ class WP_Agentic_Writer_Context_Service {
'include_images' => true,
'web_search' => false,
'default_mode' => 'writing',
'focus_keyword' => '',
'seo_focus_keyword' => '',
'seo_secondary_keywords' => '',
'seo_meta_description' => '',
@@ -336,6 +409,26 @@ class WP_Agentic_Writer_Context_Service {
return md5( $role . ':' . $content );
}
/**
* Merge context arrays while preserving nested JSON objects.
*
* @since 0.2.3
* @param array $base Existing context.
* @param array $patch Context patch.
* @return array Merged context.
*/
private function merge_context_recursive( $base, $patch ) {
foreach ( $patch as $key => $value ) {
if ( is_array( $value ) && isset( $base[ $key ] ) && is_array( $base[ $key ] ) && ! wp_is_numeric_array( $value ) ) {
$base[ $key ] = $this->merge_context_recursive( $base[ $key ], $value );
} else {
$base[ $key ] = $value;
}
}
return $base;
}
/**
* Clear context for a session and post.
*
@@ -422,4 +515,4 @@ class WP_Agentic_Writer_Context_Service {
return array_merge( array( $context_summary ), $messages );
}
}
}

View File

@@ -202,7 +202,7 @@ class WP_Agentic_Writer_Conversation_Manager {
$session = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status = 'active' ORDER BY updated_at DESC LIMIT 1",
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status != 'archived' ORDER BY updated_at DESC LIMIT 1",
$post_id
),
ARRAY_A
@@ -539,7 +539,7 @@ class WP_Agentic_Writer_Conversation_Manager {
$sessions = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$this->table_name} WHERE post_id = %d ORDER BY updated_at DESC",
"SELECT *, JSON_LENGTH(messages) as message_count FROM {$this->table_name} WHERE post_id = %d ORDER BY updated_at DESC",
$post_id
),
ARRAY_A

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,636 @@
<?php
/**
* MEMANTO Client
*
* PHP client for MEMANTO API v2 — external semantic memory service.
* Provides persistent, cross-session memory for the WP Agentic Writer plugin.
*
* @package WP_Agentic_Writer
* @since 0.3.0
*/
if (!defined("ABSPATH")) {
exit();
}
/**
* Class WP_Agentic_Writer_Memanto_Client
*
* Communicates with a MEMANTO instance (powered by Moorcheh SDK).
* All calls are wrapped in error handling — failures never disrupt the plugin.
*/
class WP_Agentic_Writer_Memanto_Client
{
/**
* Singleton instance.
*
* @var WP_Agentic_Writer_Memanto_Client
*/
private static $instance = null;
/**
* MEMANTO base URL (e.g. https://abc123.context.wpagentic.dev).
*
* @var string
*/
private $base_url = "";
/**
* Cached health status.
*
* @var array|null { healthy: bool, checked_at: int } or null if not checked yet.
*/
private $health_cache = null;
/**
* Get singleton instance.
*
* @return WP_Agentic_Writer_Memanto_Client
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct()
{
$settings = get_option("wp_agentic_writer_settings", []);
$this->base_url = untrailingslashit($settings["memanto_url"] ?? "");
}
// =========================================================================
// Configuration & Health
// =========================================================================
/**
* Whether MEMANTO is configured (URL + Moorcheh key set).
*
* @return bool
*/
public function is_configured()
{
return !empty($this->base_url) && !empty($this->get_moorcheh_key());
}
/**
* Whether MEMANTO is enabled in settings.
*
* @return bool
*/
public function is_enabled()
{
$settings = get_option("wp_agentic_writer_settings", []);
return !empty($settings["memanto_enabled"]);
}
/**
* Whether MEMANTO is enabled, configured, and reachable.
*
* @return bool
*/
public function is_active()
{
return $this->is_enabled() &&
$this->is_configured() &&
$this->is_healthy();
}
/**
* Check MEMANTO health endpoint. Result cached for 5 minutes.
*
* @return bool True if healthy.
*/
public function is_healthy()
{
if (!$this->is_configured()) {
return false;
}
// Use cached result if fresh (5 minutes).
if (null !== $this->health_cache) {
if (time() - $this->health_cache["checked_at"] < 300) {
return $this->health_cache["healthy"];
}
}
// Also check transient for cross-request caching.
$cached = get_transient("wpaw_memanto_health");
if (
false !== $cached &&
isset($cached["checked_at"]) &&
time() - $cached["checked_at"] < 300
) {
$this->health_cache = $cached;
return $cached["healthy"];
}
$response = $this->get("/health");
if (is_wp_error($response)) {
$this->health_cache = ["healthy" => false, "checked_at" => time()];
set_transient("wpaw_memanto_health", $this->health_cache, 300);
return false;
}
$healthy =
!empty($response["status"]) && "healthy" === $response["status"];
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
set_transient("wpaw_memanto_health", $this->health_cache, 300);
return $healthy;
}
/**
* Force-refresh the health check (used by Test Connection button).
*
* @return array { healthy: bool, details: array|null }
*/
public function check_health_fresh()
{
if (!$this->is_configured()) {
return ["healthy" => false, "details" => null];
}
delete_transient("wpaw_memanto_health");
$this->health_cache = null;
$response = $this->get("/health");
if (is_wp_error($response)) {
return [
"healthy" => false,
"details" => ["error" => $response->get_error_message()],
];
}
$healthy =
!empty($response["status"]) && "healthy" === $response["status"];
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
set_transient("wpaw_memanto_health", $this->health_cache, 300);
return ["healthy" => $healthy, "details" => $response];
}
// =========================================================================
// Agent Management
// =========================================================================
/**
* Ensure an agent exists. Creates if not found.
*
* @param string $agent_id Agent identifier (e.g. "wp-user-1" or "wp-post-42").
* @return bool True on success.
*/
public function ensure_agent($agent_id)
{
if (!$this->is_configured()) {
return false;
}
// Check if agent already exists.
$agent = $this->get("/api/v2/agents/{$agent_id}");
if (!is_wp_error($agent) && !empty($agent["agent_id"])) {
return true;
}
// Create agent.
$result = $this->post("/api/v2/agents", [
"agent_id" => $agent_id,
"pattern" => "support",
"description" => "WP Agentic Writer agent",
]);
return !is_wp_error($result);
}
// =========================================================================
// Session Management
// =========================================================================
/**
* Activate a session for an agent. Returns cached token if still valid.
*
* @param string $agent_id Agent identifier.
* @return string|false Session token or false on failure.
*/
public function activate_session($agent_id)
{
if (!$this->is_configured()) {
return false;
}
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
$cached_token = get_transient($transient_key);
if (!empty($cached_token)) {
return $cached_token;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/activate",
[],
$agent_id,
);
if (is_wp_error($response) || empty($response["session_token"])) {
wpaw_debug_log("MEMANTO activate_session failed", [
"agent_id" => $agent_id,
]);
return false;
}
$token = $response["session_token"];
$expires_at = strtotime($response["expires_at"] ?? "+6 hours");
$ttl = max(60, $expires_at - time() - 300); // Expire 5 min before actual.
set_transient($transient_key, $token, $ttl);
return $token;
}
/**
* Deactivate a session for an agent.
*
* Clears the cached token and sends the deactivate request
* without injecting a session token (avoids re-activation loop).
*
* @param string $agent_id Agent identifier.
* @return bool True on success.
*/
public function deactivate_session($agent_id)
{
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
delete_transient($transient_key);
if (!$this->is_configured()) {
return false;
}
$url = $this->base_url . "/api/v2/agents/{$agent_id}/deactivate";
$headers = $this->get_headers();
$response = wp_remote_post($url, [
"headers" => $headers,
"body" => wp_json_encode([]),
"timeout" => 10,
]);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO deactivate_session failed", [
"agent_id" => $agent_id,
]);
return false;
}
return true;
}
// =========================================================================
// Memory Operations
// =========================================================================
/**
* Store a memory.
*
* @param string $agent_id Agent identifier.
* @param string $content Memory content (max 10000 chars).
* @param string $type Memory type (fact, preference, goal, decision, artifact, learning, event, instruction, relationship, context, observation, commitment, error).
* @param array $tags Optional tags.
* @param string $title Optional title (max 100 chars).
* @return bool True on success.
*/
public function remember(
$agent_id,
$content,
$type = "context",
$tags = [],
$title = "",
) {
if (!$this->is_active()) {
return false;
}
$body = [
"content" => mb_substr($content, 0, 10000),
"type" => $type,
"tags" => $tags,
"source" => "wp-agentic-writer",
];
if (!empty($title)) {
$body["title"] = mb_substr($title, 0, 100);
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/remember",
$body,
$agent_id,
);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO remember failed", [
"agent_id" => $agent_id,
"type" => $type,
"error" => $response->get_error_message(),
]);
return false;
}
return true;
}
/**
* Store multiple memories in batch (max 100).
*
* @param string $agent_id Agent identifier.
* @param array $memories Array of memory items. Each item: { content, type, tags, title }.
* @return bool True on success.
*/
public function batch_remember($agent_id, $memories)
{
if (!$this->is_active() || empty($memories)) {
return false;
}
$batch = [];
foreach (array_slice($memories, 0, 100) as $item) {
$entry = [
"content" => mb_substr($item["content"] ?? "", 0, 10000),
"type" => $item["type"] ?? "context",
"source" => "wp-agentic-writer",
];
if (!empty($item["title"])) {
$entry["title"] = mb_substr($item["title"], 0, 100);
}
if (!empty($item["tags"])) {
$entry["tags"] = $item["tags"];
}
$batch[] = $entry;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/batch-remember",
["memories" => $batch],
$agent_id,
);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO batch_remember failed", [
"agent_id" => $agent_id,
"count" => count($batch),
"error" => $response->get_error_message(),
]);
return false;
}
return true;
}
/**
* Semantic search / recall memories.
*
* @param string $agent_id Agent identifier.
* @param string $query Search query.
* @param array $type_filter Optional memory type filter.
* @param int $limit Max results (default 10).
* @param float $min_similarity Minimum similarity score 0-1.
* @return array Recalled memories (empty on failure).
*/
public function recall(
$agent_id,
$query,
$type_filter = [],
$limit = 10,
$min_similarity = 0.3,
) {
if (!$this->is_active()) {
return [];
}
$body = [
"query" => $query,
"limit" => $limit,
"min_similarity" => $min_similarity,
];
if (!empty($type_filter)) {
$body["type"] = $type_filter;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/recall",
$body,
$agent_id,
);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO recall failed", [
"agent_id" => $agent_id,
"query" => substr($query, 0, 100),
"error" => $response->get_error_message(),
]);
return [];
}
return is_array($response) ? $response : [];
}
/**
* Recall most recent memories.
*
* @param string $agent_id Agent identifier.
* @param int $limit Max results.
* @param array $type_filter Optional memory type filter.
* @return array Recent memories (empty on failure).
*/
public function recall_recent($agent_id, $limit = 10, $type_filter = [])
{
if (!$this->is_active()) {
return [];
}
$body = ["limit" => $limit];
if (!empty($type_filter)) {
$body["type"] = $type_filter;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/recall/recent",
$body,
$agent_id,
);
if (is_wp_error($response)) {
return [];
}
return is_array($response) ? $response : [];
}
// =========================================================================
// Agent ID Builders
// =========================================================================
/**
* Build user-level agent ID.
*
* @param int $user_id WordPress user ID.
* @return string Agent ID like "wp-user-1".
*/
public function get_user_agent_id($user_id)
{
return "wp-user-" . absint($user_id);
}
/**
* Build post-level agent ID.
*
* @param int $post_id WordPress post ID.
* @return string Agent ID like "wp-post-42".
*/
public function get_post_agent_id($post_id)
{
return "wp-post-" . absint($post_id);
}
// =========================================================================
// HTTP Helpers (private)
// =========================================================================
/**
* Get the Moorcheh API key from settings.
*
* @return string
*/
private function get_moorcheh_key()
{
$settings = get_option("wp_agentic_writer_settings", []);
return $settings["memanto_moorcheh_key"] ?? "";
}
/**
* GET request to MEMANTO API.
*
* @param string $path API path (e.g. "/health", "/api/v2/agents/{id}").
* @return array|WP_Error Decoded response or error.
*/
private function get($path)
{
$url = $this->base_url . $path;
$response = wp_remote_get($url, [
"headers" => $this->get_headers(),
"timeout" => 10,
]);
return $this->parse_response($response);
}
/**
* POST request to MEMANTO API.
*
* @param string $path API path.
* @param array $body Request body.
* @param string $agent_id Optional agent ID for session token injection.
* @return array|WP_Error Decoded response or error.
*/
private function post($path, $body = [], $agent_id = "")
{
$url = $this->base_url . $path;
$headers = $this->get_headers();
// Inject session token if we have an agent_id.
if (!empty($agent_id)) {
$token = $this->activate_session($agent_id);
if ($token) {
$headers["X-Session-Token"] = $token;
}
}
$response = wp_remote_post($url, [
"headers" => $headers,
"body" => wp_json_encode($body),
"timeout" => 10,
]);
// Handle expired token (401) — re-activate and retry once.
if (!is_wp_error($response)) {
$code = wp_remote_retrieve_response_code($response);
if (401 === $code && !empty($agent_id)) {
// Clear cached token and retry.
delete_transient("wpaw_memanto_token_" . md5($agent_id));
$token = $this->activate_session($agent_id);
if ($token) {
$headers["X-Session-Token"] = $token;
$response = wp_remote_post($url, [
"headers" => $headers,
"body" => wp_json_encode($body),
"timeout" => 10,
]);
}
}
}
return $this->parse_response($response);
}
/**
* Build common headers for MEMANTO API requests.
*
* @return array Headers.
*/
private function get_headers()
{
return [
"Content-Type" => "application/json",
"Accept" => "application/json",
"X-API-Key" => $this->get_moorcheh_key(),
];
}
/**
* Parse HTTP response from MEMANTO API.
*
* @param array|WP_Error $response wp_remote response.
* @return array|WP_Error Decoded body or error.
*/
private function parse_response($response)
{
if (is_wp_error($response)) {
return new WP_Error(
"memanto_connection_error",
$response->get_error_message(),
);
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if (401 === $code) {
// Let caller handle re-auth.
return new WP_Error(
"memanto_unauthorized",
"Session token expired",
);
}
if ($code >= 400) {
wpaw_debug_log("MEMANTO API error ({$code})", $body);
return new WP_Error(
"memanto_api_error",
sprintf(
"MEMANTO API error (%d): %s",
$code,
substr($body, 0, 200),
),
);
}
$decoded = json_decode($body, true);
return is_array($decoded) ? $decoded : [];
}
}

View File

@@ -0,0 +1,754 @@
<?php
/**
* MEMANTO Context Enhancer
*
* Orchestrates when and what to remember/recall from MEMANTO.
* Hooks into the existing Context Service to provide memory
* enrichment without replacing any existing behavior.
*
* @package WP_Agentic_Writer
* @since 0.3.0
*/
if (!defined("ABSPATH")) {
exit();
}
/**
* Class WP_Agentic_Writer_Memanto_Context_Enhancer
*
* Every public method checks is_active() first and returns gracefully
* if MEMANTO is not available. The plugin never breaks.
*/
class WP_Agentic_Writer_Memanto_Context_Enhancer
{
/**
* Singleton instance.
*
* @var WP_Agentic_Writer_Memanto_Context_Enhancer
*/
private static $instance = null;
/**
* MEMANTO client reference.
*
* @var WP_Agentic_Writer_Memanto_Client
*/
private $client;
/**
* Get singleton instance.
*
* @return WP_Agentic_Writer_Memanto_Context_Enhancer
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*
* Registers all WordPress action/filter hooks so that the sidebar
* handlers only need a single do_action() call at each trigger point.
*/
private function __construct()
{
$this->client = WP_Agentic_Writer_Memanto_Client::get_instance();
// Session lifecycle.
add_action(
"wpaw_memanto_session_start",
[$this, "on_session_start"],
10,
3,
);
add_action(
"wpaw_memanto_session_end",
[$this, "on_session_end"],
10,
2,
);
// Write-through: remember on meaningful events.
add_action(
"wpaw_memanto_user_message",
[$this, "on_user_message"],
10,
3,
);
add_action(
"wpaw_memanto_plan_generated",
[$this, "on_plan_generated"],
10,
2,
);
add_action(
"wpaw_memanto_plan_approved",
[$this, "on_plan_approved"],
10,
2,
);
add_action(
"wpaw_memanto_plan_rejected",
[$this, "on_plan_rejected"],
10,
2,
);
add_action(
"wpaw_memanto_section_written",
[$this, "on_section_written"],
10,
3,
);
add_action(
"wpaw_memanto_block_refined",
[$this, "on_block_refined"],
10,
3,
);
add_action(
"wpaw_memanto_config_saved",
[$this, "on_config_saved"],
10,
2,
);
}
// =========================================================================
// Session Lifecycle
// =========================================================================
/**
* Called when a conversation session starts.
* Ensures agents exist and recalls previous session state.
*
* @param string $session_id Session ID.
* @param int $post_id Post ID.
* @param int $user_id WordPress user ID.
*/
public function on_session_start($session_id, $post_id, $user_id)
{
if (!$this->client->is_active()) {
return;
}
// Ensure user agent exists.
if ($user_id > 0) {
$this->client->ensure_agent(
$this->client->get_user_agent_id($user_id),
);
}
// Ensure post agent exists.
if ($post_id > 0) {
$this->client->ensure_agent(
$this->client->get_post_agent_id($post_id),
);
}
}
/**
* Called when a conversation session ends.
* Stores a session summary memory and deactivates the session.
*
* @param string $session_id Session ID.
* @param int $post_id Post ID.
*/
public function on_session_end($session_id, $post_id)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$post_agent = $this->client->get_post_agent_id($post_id);
// Store a session summary.
$this->client->remember(
$post_agent,
"Session ended: " . $session_id,
"context",
["session:" . $session_id, "post:" . $post_id],
"Session end",
);
// Deactivate MEMANTO session to trigger summary generation.
$this->client->deactivate_session($post_agent);
}
// =========================================================================
// Write-Through: Remember on Meaningful Events
// =========================================================================
/**
* Store a memory when user sends a chat message.
*
* @param string $session_id Session ID.
* @param string $content User message content.
* @param int $post_id Post ID.
*/
public function on_user_message($session_id, $content, $post_id)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"User instruction: " . wp_strip_all_tags($content),
"instruction",
["post:" . $post_id, "session:" . $session_id],
);
}
/**
* Store a memory when a plan is generated.
*
* @param int $post_id Post ID.
* @param array $plan Plan data.
*/
public function on_plan_generated($post_id, $plan)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$title = $plan["title"] ?? "Untitled";
$sections =
isset($plan["sections"]) && is_array($plan["sections"])
? count($plan["sections"])
: 0;
$this->client->remember(
$this->client->get_post_agent_id($post_id),
sprintf('Plan generated: "%s" with %d sections', $title, $sections),
"artifact",
["post:" . $post_id, "type:plan"],
"Plan: " . $title,
);
}
/**
* Store a memory when user approves a plan.
*
* @param int $post_id Post ID.
* @param array $plan Plan data.
*/
public function on_plan_approved($post_id, $plan)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$title = $plan["title"] ?? "Untitled";
$this->client->remember(
$this->client->get_post_agent_id($post_id),
sprintf('User approved plan: "%s"', $title),
"decision",
["post:" . $post_id, "type:plan"],
"Plan approved",
);
}
/**
* Store a memory when user rejects or requests plan changes.
*
* @param int $post_id Post ID.
* @param string $reason Rejection/revision reason.
*/
public function on_plan_rejected($post_id, $reason)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"User requested plan revision: " . wp_strip_all_tags($reason),
"error",
["post:" . $post_id, "type:plan"],
"Plan revision",
);
}
/**
* Store a memory when a section is written.
*
* @param int $post_id Post ID.
* @param string $section_id Section identifier.
* @param string $summary Brief section summary.
*/
public function on_section_written($post_id, $section_id, $summary)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"Section written (" .
$section_id .
"): " .
wp_strip_all_tags($summary),
"artifact",
["post:" . $post_id, "section:" . $section_id],
"Section: " . $section_id,
);
}
/**
* Store a memory when a block is refined.
*
* @param int $post_id Post ID.
* @param string $block_id Block identifier.
* @param string $instruction Refinement instruction.
*/
public function on_block_refined($post_id, $block_id, $instruction)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"Block refined (" .
$block_id .
"): " .
wp_strip_all_tags($instruction),
"instruction",
["post:" . $post_id, "block:" . $block_id],
);
}
/**
* Store a memory when post config is saved.
*
* @param int $post_id Post ID.
* @param array $config Post config data.
*/
public function on_config_saved($post_id, $config)
{
if (!$this->client->is_active()) {
return;
}
$config_summary = sprintf(
"tone=%s, audience=%s, length=%s, language=%s",
$config["tone"] ?? "default",
$config["audience"] ?? "general",
$config["article_length"] ?? "medium",
$config["language"] ?? "auto",
);
// Store to post agent.
if ($post_id > 0) {
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"Article config: " . $config_summary,
"preference",
["post:" . $post_id],
"Post config",
);
}
// Store to user agent (cross-post preferences).
$user_id = get_current_user_id();
if ($user_id > 0) {
$this->client->remember(
$this->client->get_user_agent_id($user_id),
"User preference: " . $config_summary,
"preference",
["user:" . $user_id],
"Writing preferences",
);
}
}
// =========================================================================
// Recall: Retrieve Memories for Context Enrichment
// =========================================================================
/**
* Recall relevant memories to enrich AI prompt context.
*
* @param int $post_id Post ID.
* @param int $user_id WordPress user ID.
* @param string $current_message User's current message (for semantic search).
* @return array Recalled memory items, each with 'type', 'content', 'title'.
*/
public function recall_for_context(
$post_id,
$user_id,
$current_message = "",
) {
if (!$this->client->is_active()) {
return [];
}
$memories = [];
$seen = [];
// 1. Recent post memories.
if ($post_id > 0) {
$post_agent = $this->client->get_post_agent_id($post_id);
$recent = $this->client->recall_recent($post_agent, 10);
foreach ($this->normalize_memories($recent) as $item) {
$hash = md5($item["content"]);
if (!isset($seen[$hash])) {
$seen[$hash] = true;
$memories[] = $item;
}
}
// 2. Semantic recall based on current message.
if (!empty($current_message)) {
$semantic = $this->client->recall(
$post_agent,
$current_message,
[],
5,
);
foreach ($this->normalize_memories($semantic) as $item) {
$hash = md5($item["content"]);
if (!isset($seen[$hash])) {
$seen[$hash] = true;
$memories[] = $item;
}
}
}
}
// 3. User preferences (cross-post).
if ($user_id > 0) {
$user_prefs = $this->client->recall(
$this->client->get_user_agent_id($user_id),
"writing preferences tone audience language",
["preference"],
5,
);
foreach ($this->normalize_memories($user_prefs) as $item) {
$hash = md5($item["content"]);
if (!isset($seen[$hash])) {
$seen[$hash] = true;
$memories[] = $item;
}
}
}
return $memories;
}
/**
* Restore session state from MEMANTO when reopening a post.
*
* Returns a structured restore payload that the frontend can use to:
* - Display a "Restored from memory" badge
* - Build a restored-session system message for the AI
* - Pre-fill config from prior preferences
*
* @since 0.4.0
* @param int $post_id Post ID being reopened.
* @param int $user_id Current user ID.
* @return array { restored: bool, memories: array, preferences: array, summary: string }
*/
public function restore_session($post_id, $user_id)
{
$empty = [
"restored" => false,
"memories" => [],
"preferences" => [],
"summary" => "",
];
if (!$this->client->is_active() || $post_id <= 0) {
return $empty;
}
// Recall recent post memories (no current message — restore is about history).
$post_agent = $this->client->get_post_agent_id($post_id);
$recent = $this->client->recall_recent($post_agent, 15);
$memories = $this->normalize_memories($recent);
// Recall user preferences (for cross-post config carry-over).
$preferences = [];
if ($user_id > 0) {
$user_prefs = $this->client->recall(
$this->client->get_user_agent_id($user_id),
"writing preferences tone audience language length",
["preference"],
5,
);
$preferences = $this->normalize_memories($user_prefs);
}
if (empty($memories) && empty($preferences)) {
return $empty;
}
// Build a human-readable summary for the restore badge tooltip.
$summary = $this->build_restore_summary($memories, $preferences);
return [
"restored" => true,
"memories" => $memories,
"preferences" => $preferences,
"summary" => $summary,
];
}
/**
* Build a compact summary string of restored memories.
*
* @param array $memories Restored memories.
* @param array $preferences Restored preferences.
* @return string Summary text.
*/
private function build_restore_summary($memories, $preferences)
{
$parts = [];
if (!empty($memories)) {
$parts[] = sprintf(
/* translators: %d: number of memories */
_n(
"%d memory",
"%d memories",
count($memories),
"wp-agentic-writer",
),
count($memories),
);
}
if (!empty($preferences)) {
$parts[] = sprintf(
/* translators: %d: number of preferences */
_n(
"%d preference",
"%d preferences",
count($preferences),
"wp-agentic-writer",
),
count($preferences),
);
}
return implode(", ", $parts);
}
/**
* Get user's writing preferences recalled from MEMANTO.
*
* Used when creating a new post to pre-fill the post config
* with the user's habitual tone, audience, etc.
*
* @since 0.4.0
* @param int $user_id WordPress user ID.
* @return array { restored: bool, config: array }
*/
public function get_user_preferences_for_new_post($user_id)
{
$empty = ["restored" => false, "config" => []];
if (!$this->client->is_active() || $user_id <= 0) {
return $empty;
}
$user_prefs = $this->client->recall(
$this->client->get_user_agent_id($user_id),
"writing preferences tone audience language length",
["preference"],
3,
);
$prefs = $this->normalize_memories($user_prefs);
if (empty($prefs)) {
return $empty;
}
// Extract config fields from preference content.
// Memory format: "User preference: tone=professional, audience=experts, length=long, language=en"
$config = $this->extract_config_from_preferences($prefs);
if (empty($config)) {
return $empty;
}
return ["restored" => true, "config" => $config];
}
/**
* Parse preference memory contents and extract config fields.
*
* @param array $prefs Normalized preference memories.
* @return array Extracted config: { tone, audience, article_length, language }.
*/
private function extract_config_from_preferences($prefs)
{
$config = [];
// Combine all preference content into one blob for parsing.
$blob = "";
foreach ($prefs as $pref) {
$blob .= " " . ($pref["content"] ?? "");
}
// Match key=value patterns.
if (preg_match('/tone\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "default" !== strtolower($val)) {
$config["tone"] = $val;
}
}
if (preg_match('/audience\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "general" !== strtolower($val)) {
$config["audience"] = $val;
}
}
if (preg_match('/(?:article_)?length\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "medium" !== strtolower($val)) {
$config["article_length"] = $val;
}
}
if (preg_match('/language\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "auto" !== strtolower($val)) {
$config["language"] = $val;
}
}
return $config;
}
/**
* Build a "restored session" system message for the AI.
*
* This message summarizes prior post work so the AI can resume
* mid-article without re-asking the user for context.
*
* @since 0.4.0
* @param array $restore_payload Result from restore_session().
* @return string System message content (empty if nothing to restore).
*/
public function build_session_restore_message($restore_payload)
{
if (empty($restore_payload["restored"])) {
return "";
}
$memories = $restore_payload["memories"] ?? [];
$preferences = $restore_payload["preferences"] ?? [];
if (empty($memories) && empty($preferences)) {
return "";
}
$lines = ["SESSION RESTORED FROM MEMORY"];
$lines[] =
"The user has returned to this post. Below is context from prior sessions.";
$lines[] =
"Use this to continue where the conversation left off without re-asking.";
// Summarize prior post work.
if (!empty($memories)) {
$lines[] = "";
$lines[] = "## Prior session activity";
$grouped = [];
foreach ($memories as $memory) {
$type = $memory["type"] ?? "context";
if (!isset($grouped[$type])) {
$grouped[$type] = [];
}
$grouped[$type][] = $memory;
}
// Render in priority order: plan > decision > instruction > artifact > error > context.
$priority = [
"artifact",
"decision",
"instruction",
"error",
"learning",
"context",
"preference",
];
foreach ($priority as $type) {
if (empty($grouped[$type])) {
continue;
}
$type_label = ucfirst($type) . "s";
$lines[] = "### {$type_label}";
foreach ($grouped[$type] as $m) {
$content = trim($m["content"] ?? "");
if ("" === $content) {
continue;
}
$title = !empty($m["title"])
? " [" . trim($m["title"]) . "]"
: "";
$lines[] = "- {$content}{$title}";
}
}
}
// Append user preferences (compact).
if (!empty($preferences)) {
$lines[] = "";
$lines[] = "## User writing preferences";
foreach ($preferences as $pref) {
$content = trim($pref["content"] ?? "");
if ("" !== $content) {
$lines[] = "- {$content}";
}
}
}
return implode("\n", $lines);
}
/**
* Normalize raw MEMANTO response into a uniform array.
*
* @param array $raw Raw response from recall/recall_recent.
* @return array Normalized items: { type, content, title }.
*/
private function normalize_memories($raw)
{
if (!is_array($raw)) {
return [];
}
// Handle different possible response shapes.
$items = $raw;
// If response has a 'memories' or 'results' key, unwrap.
if (isset($raw["memories"]) && is_array($raw["memories"])) {
$items = $raw["memories"];
} elseif (isset($raw["results"]) && is_array($raw["results"])) {
$items = $raw["results"];
}
$normalized = [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$normalized[] = [
"type" => $item["type"] ?? "context",
"content" => $item["content"] ?? ($item["text"] ?? ""),
"title" => $item["title"] ?? "",
];
}
return $normalized;
}
}

View File

@@ -514,6 +514,49 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
return __( 'Please go to Settings → Models and select a different model that is available on OpenRouter.', 'wp-agentic-writer' );
}
/**
* Build optional request-level OpenRouter provider routing preferences.
*
* This is intentionally settings-driven. BYOK users may pin a provider and
* disable fallbacks, but the plugin should not assume every OpenRouter model
* should use OpenAI, Anthropic, Azure, or any other provider.
*
* @since 0.2.3
* @param array $options Request options.
* @return array Provider routing preferences.
*/
private function get_provider_routing_preferences( $options = array() ) {
if ( isset( $options['provider'] ) && is_array( $options['provider'] ) ) {
return $options['provider'];
}
if ( array_key_exists( 'openrouter_provider_routing', $options ) && false === (bool) $options['openrouter_provider_routing'] ) {
return array();
}
$settings = get_option( 'wp_agentic_writer_settings', array() );
$enabled = ! empty( $settings['openrouter_provider_routing_enabled'] );
$provider_slug = isset( $settings['openrouter_provider_slug'] ) ? sanitize_key( $settings['openrouter_provider_slug'] ) : '';
if ( ! $enabled || '' === $provider_slug || 'auto' === $provider_slug ) {
return array();
}
$routing = array(
'order' => array( $provider_slug ),
);
if ( ! empty( $settings['openrouter_provider_only'] ) ) {
$routing['only'] = array( $provider_slug );
}
if ( isset( $settings['openrouter_allow_provider_fallbacks'] ) ) {
$routing['allow_fallbacks'] = (bool) $settings['openrouter_allow_provider_fallbacks'];
}
return $routing;
}
/**
* Get singleton instance.
*
@@ -605,6 +648,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
'include' => true,
),
);
$provider_routing = $this->get_provider_routing_preferences( $options );
if ( ! empty( $provider_routing ) ) {
$body['provider'] = $provider_routing;
}
// Add optional parameters.
if ( isset( $options['max_tokens'] ) ) {
@@ -737,7 +784,21 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
// Validate model availability before making API call
$model_validation = $this->validate_model_availability( $model );
if ( is_wp_error( $model_validation ) ) {
return $model_validation;
// Auto-fallback: try registry fallback model instead of hard-failing
$fallback_model = WPAW_Model_Registry::get_fallback_model( $type );
if ( $fallback_model && $fallback_model !== $model ) {
$fallback_validation = $this->validate_model_availability( $fallback_model );
if ( true === $fallback_validation ) {
$model = $fallback_model;
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "WPAW: Model unavailable, auto-fallback to: {$fallback_model}" );
}
} else {
return $model_validation;
}
} else {
return $model_validation;
}
}
// Build request body.
@@ -752,6 +813,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
'include' => true,
),
);
$provider_routing = $this->get_provider_routing_preferences( $options );
if ( ! empty( $provider_routing ) ) {
$body['provider'] = $provider_routing;
}
// Add optional parameters.
if ( isset( $options['max_tokens'] ) ) {

View File

@@ -43,6 +43,7 @@ class WP_Agentic_Writer_Provider_Manager {
public static function get_provider_for_task( $type ) {
$settings = get_option( 'wp_agentic_writer_settings', array() );
$task_providers = $settings['task_providers'] ?? array();
$allow_openrouter_fallback = ! empty( $settings['allow_openrouter_fallback'] );
// Determine which provider to use for this task
$requested_provider = $task_providers[ $type ] ?? 'openrouter';
@@ -58,11 +59,26 @@ class WP_Agentic_Writer_Provider_Manager {
// Get provider instance with fallback logic
$provider = self::get_provider_instance( $requested_provider, $type );
// If provider not configured or unavailable, fallback to OpenRouter
$can_fallback_to_openrouter = ( 'openrouter' === $requested_provider ) || $allow_openrouter_fallback;
// If provider not configured or unavailable.
if ( ! $provider || ! $provider->is_configured() ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "Provider '{$requested_provider}' not available for task '{$type}', using OpenRouter fallback" );
error_log( "Provider '{$requested_provider}' not available for task '{$type}'" );
}
// Never silently spend OpenRouter credits when user selected another provider.
if ( ! $can_fallback_to_openrouter ) {
$warnings[] = "Provider '{$requested_provider}' unavailable. No automatic fallback was applied.";
return new WPAW_Provider_Selection_Result(
$provider,
$requested_provider,
$requested_provider,
false,
$warnings
);
}
$warnings[] = "Provider '{$requested_provider}' unavailable, fell back to OpenRouter";
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$actual_provider = 'openrouter';
@@ -74,12 +90,16 @@ class WP_Agentic_Writer_Provider_Manager {
$test_result = $provider->test_connection();
if ( is_wp_error( $test_result ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "Local Backend not reachable for task '{$type}', using OpenRouter fallback. Error: " . $test_result->get_error_message() );
error_log( "Local Backend not reachable for task '{$type}'. Error: " . $test_result->get_error_message() );
}
if ( $can_fallback_to_openrouter ) {
$warnings[] = "Local Backend not reachable, fell back to OpenRouter.";
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$actual_provider = 'openrouter';
$fallback_used = true;
} else {
$warnings[] = "Local Backend not reachable. No automatic fallback was applied.";
}
$warnings[] = "Local Backend not reachable, fell back to OpenRouter";
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$actual_provider = 'openrouter';
$fallback_used = true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Build a clean distributable ZIP for Local Backend package.
#
# Usage:
# ./scripts/build-local-backend-zip.sh
# ./scripts/build-local-backend-zip.sh /path/to/source /path/to/output.zip
SOURCE_DIR="${1:-/Users/dwindown/Documents/agentic-writer-local-backend}"
OUTPUT_ZIP="${2:-/private/tmp/agentic-writer-local-backend.zip}"
if [[ ! -d "$SOURCE_DIR" ]]; then
echo "Source directory not found: $SOURCE_DIR" >&2
exit 1
fi
if ! command -v zip >/dev/null 2>&1; then
echo "'zip' command is required but not found." >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
PKG_DIR="$TMP_DIR/agentic-writer-local-backend"
mkdir -p "$PKG_DIR"
# Copy only distributable files/directories (exclude runtime/build/noise files).
rsync -a \
--exclude '.git/' \
--exclude '.github/' \
--exclude '.claude/' \
--exclude '.sixth/' \
--exclude 'node_modules/' \
--exclude '.env' \
--exclude '.env.*' \
--exclude '*.log' \
--exclude 'logs/' \
--exclude '.DS_Store' \
--exclude '__MACOSX/' \
--exclude '*.zip' \
"$SOURCE_DIR/" "$PKG_DIR/"
mkdir -p "$(dirname "$OUTPUT_ZIP")"
rm -f "$OUTPUT_ZIP"
(
cd "$TMP_DIR"
zip -r "$OUTPUT_ZIP" "agentic-writer-local-backend" >/dev/null
)
echo "Built package:"
echo " Source: $SOURCE_DIR"
echo " Output: $OUTPUT_ZIP"

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

@@ -48,10 +48,33 @@ if ( ! defined( 'ABSPATH' ) ) {
<div class="col-6 col-md-3">
<div class="p-3 rounded bg-warning bg-opacity-10 text-center">
<div class="fs-4 fw-bold text-warning" id="wpaw-stat-avg">$0.0000</div>
<div class="text-muted small"><?php esc_html_e( 'Avg Per Post', 'wp-agentic-writer' ); ?></div>
<div class="text-muted small d-flex justify-content-center align-items-center gap-1">
<span><?php esc_html_e( 'Avg Per Post', 'wp-agentic-writer' ); ?></span>
<span class="dashicons dashicons-info-outline" title="<?php esc_attr_e( 'All-time OpenRouter cost divided by total posts with OpenRouter usage.', 'wp-agentic-writer' ); ?>"></span>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" id="wpaw-action-summary-table">
<thead class="table-light">
<tr>
<th><?php esc_html_e( 'Action', 'wp-agentic-writer' ); ?></th>
<th class="text-end"><?php esc_html_e( 'Calls', 'wp-agentic-writer' ); ?></th>
<th class="text-end"><?php esc_html_e( 'Total Cost', 'wp-agentic-writer' ); ?></th>
<th class="text-end"><?php esc_html_e( 'Avg / Call', 'wp-agentic-writer' ); ?></th>
</tr>
</thead>
<tbody id="wpaw-action-summary-tbody">
<tr>
<td colspan="4" class="text-center text-muted py-3"><?php esc_html_e( 'Loading action summary...', 'wp-agentic-writer' ); ?></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -66,7 +66,15 @@ if ( $ai_client_available ) {
<span class="text-danger ms-2">*</span>
</label>
<div class="form-text">
<?php printf( wp_kses_post( __( 'Get your API key from <a href="%s" target="_blank" class="text-decoration-none">OpenRouter <i class="bi bi-box-arrow-up-right"></i></a>', 'wp-agentic-writer' ) ), 'https://openrouter.ai/keys' ); ?>
<?php printf(
wp_kses_post(
__(
'Get your API key from <a href="%s" target="_blank" class="text-decoration-none">OpenRouter <i class="bi bi-box-arrow-up-right"></i></a>',
"wp-agentic-writer",
),
),
"https://openrouter.ai/keys",
); ?>
</div>
<div class="input-group">
<span class="input-group-text"><span class="dashicons dashicons-admin-network"></span></span>

View File

@@ -218,6 +218,18 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php endforeach; ?>
</tbody>
</table>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="wpaw-allow-openrouter-fallback"
name="wp_agentic_writer_settings[allow_openrouter_fallback]" value="1"
<?php checked( ! empty( $allow_openrouter_fallback ) ); ?> />
<label class="form-check-label" for="wpaw-allow-openrouter-fallback">
<?php esc_html_e( 'Allow automatic fallback to OpenRouter when selected provider fails', 'wp-agentic-writer' ); ?>
</label>
<div class="form-text text-warning">
<?php esc_html_e( 'Off by default to prevent unexpected OpenRouter charges.', 'wp-agentic-writer' ); ?>
</div>
</div>
</div>
</div>
</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>

View File

@@ -119,6 +119,66 @@ if ( ! function_exists( 'wpaw_get_provider_badge' ) ) {
<div class="card-body">
<div id="wpaw-models-message" class="alert d-none mb-3"></div>
<div class="border rounded p-3 mb-4">
<div class="d-flex align-items-start justify-content-between gap-3">
<div>
<h6 class="mb-1"><?php esc_html_e( 'OpenRouter Provider Routing', 'wp-agentic-writer' ); ?></h6>
<p class="text-muted small mb-0"><?php esc_html_e( 'Optional. Use this to pin OpenRouter requests to a provider such as OpenAI, Anthropic, Google, or Z.ai when you manage BYOK/provider routing in OpenRouter.', 'wp-agentic-writer' ); ?></p>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
id="openrouter_provider_routing_enabled"
name="wp_agentic_writer_settings[openrouter_provider_routing_enabled]"
value="1"
<?php checked( ! empty( $openrouter_provider_routing_enabled ) ); ?>>
<label class="form-check-label small" for="openrouter_provider_routing_enabled">
<?php esc_html_e( 'Enable', 'wp-agentic-writer' ); ?>
</label>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-4">
<label for="openrouter_provider_slug" class="form-label small fw-semibold">
<?php esc_html_e( 'Provider slug', 'wp-agentic-writer' ); ?>
</label>
<input type="text"
id="openrouter_provider_slug"
name="wp_agentic_writer_settings[openrouter_provider_slug]"
value="<?php echo esc_attr( $openrouter_provider_slug ?? 'auto' ); ?>"
class="form-control form-control-sm"
placeholder="openai">
<div class="form-text"><?php esc_html_e( 'Examples: openai, anthropic, google, z-ai. Use OpenRouter provider slugs.', 'wp-agentic-writer' ); ?></div>
</div>
<div class="col-md-4">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox"
id="openrouter_provider_only"
name="wp_agentic_writer_settings[openrouter_provider_only]"
value="1"
<?php checked( ! empty( $openrouter_provider_only ) ); ?>>
<label class="form-check-label" for="openrouter_provider_only">
<?php esc_html_e( 'Only use this provider', 'wp-agentic-writer' ); ?>
</label>
</div>
<div class="form-text"><?php esc_html_e( 'Prevents Azure or other providers when the slug is openai.', 'wp-agentic-writer' ); ?></div>
</div>
<div class="col-md-4">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox"
id="openrouter_allow_provider_fallbacks"
name="wp_agentic_writer_settings[openrouter_allow_provider_fallbacks]"
value="1"
<?php checked( ! empty( $openrouter_allow_provider_fallbacks ) ); ?>>
<label class="form-check-label" for="openrouter_allow_provider_fallbacks">
<?php esc_html_e( 'Allow fallback providers', 'wp-agentic-writer' ); ?>
</label>
</div>
<div class="form-text"><?php esc_html_e( 'Leave off for BYOK-only behavior. Also enable Always use for this provider in OpenRouter.', 'wp-agentic-writer' ); ?></div>
</div>
</div>
</div>
<div class="row g-4">
<!-- Chat Model -->
<div class="col-md-6">

View File

@@ -108,11 +108,6 @@ function wp_agentic_writer_init() {
WP_Agentic_Writer_Admin_Columns::get_instance();
}
// Debug: Log plugin URL (only when SCRIPT_DEBUG is enabled).
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
error_log( 'WP Agentic Writer URL: ' . WP_AGENTIC_WRITER_URL );
error_log( 'WP Agentic Writer DIR: ' . WP_AGENTIC_WRITER_DIR );
}
}
add_action( 'plugins_loaded', 'wp_agentic_writer_init' );