Compare commits
6 Commits
main
...
619d36d3c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619d36d3c8 | ||
|
|
379a72e52d | ||
|
|
23a34b3035 | ||
|
|
b4ea9025b1 | ||
|
|
f7bf1f5153 | ||
|
|
ae70e4aea9 |
23
TASKLIST_AUDIT_FIXES.md
Normal file
23
TASKLIST_AUDIT_FIXES.md
Normal 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
|
||||||
@@ -18,6 +18,10 @@
|
|||||||
color: var(--wpaw-primary);
|
color: var(--wpaw-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-check.mt-3 input[type=checkbox] {
|
||||||
|
margin-top: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Card enhancements */
|
/* Card enhancements */
|
||||||
.wpaw-settings-v2-wrap .card {
|
.wpaw-settings-v2-wrap .card {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
|||||||
models: {},
|
models: {},
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
perPage: 25,
|
perPage: 25,
|
||||||
|
childPerPage: 20,
|
||||||
filters: {
|
filters: {
|
||||||
post: '',
|
post: '',
|
||||||
model: '',
|
model: '',
|
||||||
@@ -475,6 +476,7 @@
|
|||||||
if (response.success) {
|
if (response.success) {
|
||||||
renderCostLogTable(response.data);
|
renderCostLogTable(response.data);
|
||||||
updateCostLogStats(response.data.stats);
|
updateCostLogStats(response.data.stats);
|
||||||
|
renderActionSummary(response.data.stats);
|
||||||
updateFilterOptions(response.data.filters);
|
updateFilterOptions(response.data.filters);
|
||||||
renderPagination(response.data);
|
renderPagination(response.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -507,6 +509,8 @@
|
|||||||
let html = '';
|
let html = '';
|
||||||
records.forEach((group, index) => {
|
records.forEach((group, index) => {
|
||||||
const collapseId = `collapse-post-${group.post_id}-${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
|
const postCell = group.post_link
|
||||||
? `<a href="${group.post_link}" class="text-decoration-none" target="_blank">${escapeHtml(group.post_title)}</a>`
|
? `<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>`;
|
: `<span class="text-muted">${escapeHtml(group.post_title)}</span>`;
|
||||||
@@ -526,9 +530,13 @@
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Collapsible details row
|
// 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 += `
|
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">
|
<td colspan="3" class="p-0">
|
||||||
|
${detailsHint}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm mb-0 wpaw-details-table">
|
<table class="table table-sm mb-0 wpaw-details-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -541,7 +549,7 @@
|
|||||||
<th class="text-end px-3 small text-muted"><?php esc_html_e( 'Cost', 'wp-agentic-writer' ); ?></th>
|
<th class="text-end px-3 small text-muted"><?php esc_html_e( 'Cost', 'wp-agentic-writer' ); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="wpaw-details-body">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Detail rows
|
// Detail rows
|
||||||
@@ -562,6 +570,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -577,15 +591,55 @@
|
|||||||
const isExpanded = $(target).hasClass('show');
|
const isExpanded = $(target).hasClass('show');
|
||||||
$icon.toggleClass('dashicons-arrow-right-alt2', !isExpanded);
|
$icon.toggleClass('dashicons-arrow-right-alt2', !isExpanded);
|
||||||
$icon.toggleClass('dashicons-arrow-down-alt2', isExpanded);
|
$icon.toggleClass('dashicons-arrow-down-alt2', isExpanded);
|
||||||
|
if (isExpanded) {
|
||||||
|
renderChildPage($(target), 1);
|
||||||
|
}
|
||||||
}, 10);
|
}, 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
|
// Update records info
|
||||||
const start = (data.current_page - 1) * data.per_page + 1;
|
const start = (data.current_page - 1) * data.per_page + 1;
|
||||||
const end = Math.min(data.current_page * data.per_page, data.total_items);
|
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`);
|
$('#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
|
* Update cost log stats
|
||||||
*/
|
*/
|
||||||
@@ -597,6 +651,34 @@
|
|||||||
$('#wpaw-stat-avg').text('$' + stats.avg_per_post);
|
$('#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
|
* Update filter dropdown options
|
||||||
*/
|
*/
|
||||||
|
|||||||
18434
assets/js/sidebar.js
18434
assets/js/sidebar.js
File diff suppressed because it is too large
Load Diff
421
docs/architecture/OPENROUTER_BYOK_CONTEXT_STREAMING_SPEC.md
Normal file
421
docs/architecture/OPENROUTER_BYOK_CONTEXT_STREAMING_SPEC.md
Normal 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.
|
||||||
435
docs/implementation/MEMANTO_INTEGRATION_PLAN.md
Normal file
435
docs/implementation/MEMANTO_INTEGRATION_PLAN.md
Normal 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
|
||||||
7
docs/user-facing/downloads/.gitignore
vendored
7
docs/user-facing/downloads/.gitignore
vendored
@@ -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
|
|
||||||
Binary file not shown.
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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('═══════════════════════════════════════════════════');
|
|
||||||
});
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Binary file not shown.
@@ -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('═══════════════════════════════════════════════════');
|
|
||||||
});
|
|
||||||
@@ -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"
|
|
||||||
828
downloads/package-lock.json
generated
828
downloads/package-lock.json
generated
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
744
includes/class-context-builder.php
Normal file
744
includes/class-context-builder.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,78 @@ class WP_Agentic_Writer_Context_Service {
|
|||||||
return $manager->update_messages( $session_id, $messages );
|
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.
|
* Save plan to post meta.
|
||||||
*
|
*
|
||||||
@@ -254,6 +326,7 @@ class WP_Agentic_Writer_Context_Service {
|
|||||||
'include_images' => true,
|
'include_images' => true,
|
||||||
'web_search' => false,
|
'web_search' => false,
|
||||||
'default_mode' => 'writing',
|
'default_mode' => 'writing',
|
||||||
|
'focus_keyword' => '',
|
||||||
'seo_focus_keyword' => '',
|
'seo_focus_keyword' => '',
|
||||||
'seo_secondary_keywords' => '',
|
'seo_secondary_keywords' => '',
|
||||||
'seo_meta_description' => '',
|
'seo_meta_description' => '',
|
||||||
@@ -336,6 +409,26 @@ class WP_Agentic_Writer_Context_Service {
|
|||||||
return md5( $role . ':' . $content );
|
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.
|
* Clear context for a session and post.
|
||||||
*
|
*
|
||||||
@@ -422,4 +515,4 @@ class WP_Agentic_Writer_Context_Service {
|
|||||||
|
|
||||||
return array_merge( array( $context_summary ), $messages );
|
return array_merge( array( $context_summary ), $messages );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class WP_Agentic_Writer_Conversation_Manager {
|
|||||||
|
|
||||||
$session = $wpdb->get_row(
|
$session = $wpdb->get_row(
|
||||||
$wpdb->prepare(
|
$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
|
$post_id
|
||||||
),
|
),
|
||||||
ARRAY_A
|
ARRAY_A
|
||||||
@@ -539,7 +539,7 @@ class WP_Agentic_Writer_Conversation_Manager {
|
|||||||
|
|
||||||
$sessions = $wpdb->get_results(
|
$sessions = $wpdb->get_results(
|
||||||
$wpdb->prepare(
|
$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
|
$post_id
|
||||||
),
|
),
|
||||||
ARRAY_A
|
ARRAY_A
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -342,32 +342,44 @@ class WP_Agentic_Writer_Local_Backend_Provider implements WP_Agentic_Writer_AI_P
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test /ping endpoint
|
// Best-effort reachability checks. Do not hard-fail here; inference test below is authoritative.
|
||||||
$ping_response = wp_remote_get(
|
$reachable = false;
|
||||||
$this->base_url . '/ping',
|
$health_endpoints = array( '/ping', '/health', '/' );
|
||||||
array(
|
foreach ( $health_endpoints as $endpoint ) {
|
||||||
'timeout' => 5,
|
$health_response = wp_remote_get(
|
||||||
'sslverify' => false,
|
$this->base_url . $endpoint,
|
||||||
)
|
array(
|
||||||
);
|
'timeout' => 5,
|
||||||
|
'sslverify' => false,
|
||||||
if ( is_wp_error( $ping_response ) ) {
|
|
||||||
return new WP_Error(
|
|
||||||
'ping_failed',
|
|
||||||
sprintf(
|
|
||||||
/* translators: %s: error message */
|
|
||||||
__( 'Cannot reach proxy: %s. Is it running?', 'wp-agentic-writer' ),
|
|
||||||
$ping_response->get_error_message()
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
$ping_body = wp_remote_retrieve_body( $ping_response );
|
if ( is_wp_error( $health_response ) ) {
|
||||||
if ( 'pong' !== $ping_body ) {
|
continue;
|
||||||
return new WP_Error(
|
}
|
||||||
'invalid_ping',
|
|
||||||
__( 'Proxy responded but with unexpected format', 'wp-agentic-writer' )
|
$health_body = trim( (string) wp_remote_retrieve_body( $health_response ) );
|
||||||
);
|
$health_code = (int) wp_remote_retrieve_response_code( $health_response );
|
||||||
|
$health_json = json_decode( $health_body, true );
|
||||||
|
|
||||||
|
// Any 2xx indicates proxy process is reachable.
|
||||||
|
if ( $health_code >= 200 && $health_code < 300 ) {
|
||||||
|
$reachable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stronger signal for known proxy responses.
|
||||||
|
if ( strcasecmp( $health_body, 'pong' ) === 0 ) {
|
||||||
|
$reachable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ( is_array( $health_json ) ) {
|
||||||
|
$ok_flag = $health_json['ok'] ?? $health_json['success'] ?? null;
|
||||||
|
$status = strtolower( (string) ( $health_json['status'] ?? '' ) );
|
||||||
|
if ( true === $ok_flag || in_array( $status, array( 'ok', 'healthy', 'pong' ), true ) ) {
|
||||||
|
$reachable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test actual inference with simple prompt
|
// Test actual inference with simple prompt
|
||||||
@@ -393,6 +405,17 @@ class WP_Agentic_Writer_Local_Backend_Provider implements WP_Agentic_Writer_AI_P
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ( is_wp_error( $test_response ) ) {
|
if ( is_wp_error( $test_response ) ) {
|
||||||
|
// If both health and inference are unreachable, report connection issue.
|
||||||
|
if ( ! $reachable ) {
|
||||||
|
return new WP_Error(
|
||||||
|
'ping_failed',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: error message */
|
||||||
|
__( 'Cannot reach proxy: %s. Is it running and reachable from this server?', 'wp-agentic-writer' ),
|
||||||
|
$test_response->get_error_message()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'inference_failed',
|
'inference_failed',
|
||||||
sprintf(
|
sprintf(
|
||||||
|
|||||||
636
includes/class-memanto-client.php
Normal file
636
includes/class-memanto-client.php
Normal 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 : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
754
includes/class-memanto-context-enhancer.php
Normal file
754
includes/class-memanto-context-enhancer.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' );
|
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.
|
* Get singleton instance.
|
||||||
*
|
*
|
||||||
@@ -605,6 +648,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
|
|||||||
'include' => true,
|
'include' => true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
$provider_routing = $this->get_provider_routing_preferences( $options );
|
||||||
|
if ( ! empty( $provider_routing ) ) {
|
||||||
|
$body['provider'] = $provider_routing;
|
||||||
|
}
|
||||||
|
|
||||||
// Add optional parameters.
|
// Add optional parameters.
|
||||||
if ( isset( $options['max_tokens'] ) ) {
|
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
|
// Validate model availability before making API call
|
||||||
$model_validation = $this->validate_model_availability( $model );
|
$model_validation = $this->validate_model_availability( $model );
|
||||||
if ( is_wp_error( $model_validation ) ) {
|
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.
|
// Build request body.
|
||||||
@@ -752,6 +813,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
|
|||||||
'include' => true,
|
'include' => true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
$provider_routing = $this->get_provider_routing_preferences( $options );
|
||||||
|
if ( ! empty( $provider_routing ) ) {
|
||||||
|
$body['provider'] = $provider_routing;
|
||||||
|
}
|
||||||
|
|
||||||
// Add optional parameters.
|
// Add optional parameters.
|
||||||
if ( isset( $options['max_tokens'] ) ) {
|
if ( isset( $options['max_tokens'] ) ) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class WP_Agentic_Writer_Provider_Manager {
|
|||||||
public static function get_provider_for_task( $type ) {
|
public static function get_provider_for_task( $type ) {
|
||||||
$settings = get_option( 'wp_agentic_writer_settings', array() );
|
$settings = get_option( 'wp_agentic_writer_settings', array() );
|
||||||
$task_providers = $settings['task_providers'] ?? array();
|
$task_providers = $settings['task_providers'] ?? array();
|
||||||
|
$allow_openrouter_fallback = ! empty( $settings['allow_openrouter_fallback'] );
|
||||||
|
|
||||||
// Determine which provider to use for this task
|
// Determine which provider to use for this task
|
||||||
$requested_provider = $task_providers[ $type ] ?? 'openrouter';
|
$requested_provider = $task_providers[ $type ] ?? 'openrouter';
|
||||||
@@ -58,11 +59,26 @@ class WP_Agentic_Writer_Provider_Manager {
|
|||||||
// Get provider instance with fallback logic
|
// Get provider instance with fallback logic
|
||||||
$provider = self::get_provider_instance( $requested_provider, $type );
|
$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 ( ! $provider || ! $provider->is_configured() ) {
|
||||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
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";
|
$warnings[] = "Provider '{$requested_provider}' unavailable, fell back to OpenRouter";
|
||||||
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
||||||
$actual_provider = 'openrouter';
|
$actual_provider = 'openrouter';
|
||||||
@@ -74,12 +90,16 @@ class WP_Agentic_Writer_Provider_Manager {
|
|||||||
$test_result = $provider->test_connection();
|
$test_result = $provider->test_connection();
|
||||||
if ( is_wp_error( $test_result ) ) {
|
if ( is_wp_error( $test_result ) ) {
|
||||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
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
55
scripts/build-local-backend-zip.sh
Executable file
55
scripts/build-local-backend-zip.sh
Executable 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"
|
||||||
@@ -6,24 +6,24 @@
|
|||||||
* @var array $view_data Prepared view data from class-settings-v2.php
|
* @var array $view_data Prepared view data from class-settings-v2.php
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if (!defined("ABSPATH")) {
|
||||||
exit;
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract view data for easier access
|
// Extract view data for easier access
|
||||||
extract( $view_data );
|
extract($view_data);
|
||||||
?>
|
?>
|
||||||
<div class="wrap wpaw-settings-v2-wrap">
|
<div class="wrap wpaw-settings-v2-wrap">
|
||||||
<!-- Agentic IDE Split View Layout -->
|
<!-- Agentic IDE Split View Layout -->
|
||||||
<div class="wpaw-ide-container d-flex">
|
<div class="wpaw-ide-container d-flex">
|
||||||
|
|
||||||
<!-- Left Sidebar: Settings Navigation -->
|
<!-- Left Sidebar: Settings Navigation -->
|
||||||
<div class="wpaw-sidebar-nav flex-shrink-0">
|
<div class="wpaw-sidebar-nav flex-shrink-0">
|
||||||
<!-- Header inside Sidebar -->
|
<!-- Header inside Sidebar -->
|
||||||
<div class="wpaw-sidebar-header p-3 mb-2 border-bottom border-dark">
|
<div class="wpaw-sidebar-header p-3 mb-2 border-bottom border-dark">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<img src="<?php echo esc_url( WP_AGENTIC_WRITER_URL . 'assets/img/icon.svg' ); ?>"
|
<img src="<?php echo esc_url(WP_AGENTIC_WRITER_URL . "assets/img/icon.svg"); ?>"
|
||||||
alt="WP Agentic Writer"
|
alt="WP Agentic Writer"
|
||||||
style="width: 24px; height: 24px; filter: invert(1)">
|
style="width: 24px; height: 24px; filter: invert(1)">
|
||||||
<h1 class="h6 mb-0 text-white fw-bold">Agentic Writer</h1>
|
<h1 class="h6 mb-0 text-white fw-bold">Agentic Writer</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,19 +39,25 @@ extract( $view_data );
|
|||||||
<li class="nav-item" role="presentation">
|
<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">
|
<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>
|
<i class="bi bi-sliders"></i>
|
||||||
<?php esc_html_e( 'General', 'wp-agentic-writer' ); ?>
|
<?php esc_html_e("General", "wp-agentic-writer"); ?>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<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">
|
<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>
|
<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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<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">
|
<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>
|
<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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -59,13 +65,13 @@ extract( $view_data );
|
|||||||
<li class="nav-item" role="presentation">
|
<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">
|
<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>
|
<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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<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">
|
<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>
|
<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>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -75,7 +81,7 @@ extract( $view_data );
|
|||||||
<!-- Right Content Pane: Settings Forms -->
|
<!-- Right Content Pane: Settings Forms -->
|
||||||
<div class="wpaw-content-pane flex-grow-1 d-flex flex-column h-100">
|
<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">
|
<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 -->
|
<!-- Workflow Pipeline Progress -->
|
||||||
<div class="wpaw-workflow-progress wpaw-workflow-compact mb-4" id="wpaw-workflow-display">
|
<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>
|
<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>
|
<p class="text-secondary small mt-1">Configure global API keys, budget, and content parameters.</p>
|
||||||
</div>
|
</div>
|
||||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-general.php'; ?>
|
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-general.php"; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Models Tab -->
|
<!-- Models Tab -->
|
||||||
@@ -149,7 +155,7 @@ extract( $view_data );
|
|||||||
<h2 class="h4 text-white m-0">AI Models</h2>
|
<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>
|
<p class="text-secondary small mt-1">Select logic engines for different stages of the writing pipeline.</p>
|
||||||
</div>
|
</div>
|
||||||
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-models.php'; ?>
|
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-models.php"; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Local Backend Tab -->
|
<!-- Local Backend Tab -->
|
||||||
@@ -158,16 +164,25 @@ extract( $view_data );
|
|||||||
<h2 class="h4 text-white m-0">Local Backend</h2>
|
<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>
|
<p class="text-secondary small mt-1">Configure connections to local LM Studio or Ollama instances.</p>
|
||||||
</div>
|
</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>
|
</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="tab-pane fade" id="cost-log" role="tabpanel" aria-labelledby="cost-log-tab">
|
||||||
<div class="mb-4 pb-3 border-bottom border-dark">
|
<div class="mb-4 pb-3 border-bottom border-dark">
|
||||||
<h2 class="h4 text-white m-0">OpenRouter Cost Analytics</h2>
|
<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>
|
<p class="text-secondary small mt-1">Track API token usage and expenses across all generations.</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Guide Tab -->
|
<!-- Guide Tab -->
|
||||||
@@ -176,7 +191,7 @@ extract( $view_data );
|
|||||||
<h2 class="h4 text-white m-0">Provider Documentation</h2>
|
<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>
|
<p class="text-secondary small mt-1">Reference materials for selecting the right model constraints.</p>
|
||||||
</div>
|
</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>
|
</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="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">
|
<div class="text-secondary small d-flex align-items-center gap-2">
|
||||||
<span class="dashicons dashicons-plugin text-primary"></span>
|
<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>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="wpaw-reset-settings">
|
<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>
|
||||||
<button type="submit" class="btn btn-sm btn-primary px-4 fw-semibold" id="wpaw-save-settings">
|
<button type="submit" class="btn btn-sm btn-primary px-4 fw-semibold" id="wpaw-save-settings">
|
||||||
<?php
|
<?php
|
||||||
$is_mac = isset( $_SERVER['HTTP_USER_AGENT'] ) && strpos( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ), 'Mac OS' ) !== false;
|
$is_mac =
|
||||||
$cmd_key = $is_mac ? '⌘' : 'Ctrl';
|
isset($_SERVER["HTTP_USER_AGENT"]) &&
|
||||||
?>
|
strpos(wp_unslash($_SERVER["HTTP_USER_AGENT"]), "Mac OS") !== false;
|
||||||
<?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>
|
$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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +235,10 @@ extract( $view_data );
|
|||||||
<div id="wpaw-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
<div id="wpaw-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="toast-header">
|
<div class="toast-header">
|
||||||
<span class="me-2">✨</span>
|
<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>
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-body" id="wpaw-toast-message"></div>
|
<div class="toast-body" id="wpaw-toast-message"></div>
|
||||||
|
|||||||
@@ -48,10 +48,33 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
<div class="p-3 rounded bg-warning bg-opacity-10 text-center">
|
<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="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>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -218,6 +218,18 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
195
views/settings/tab-memanto.php
Normal file
195
views/settings/tab-memanto.php
Normal 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>
|
||||||
@@ -119,6 +119,66 @@ if ( ! function_exists( 'wpaw_get_provider_badge' ) ) {
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="wpaw-models-message" class="alert d-none mb-3"></div>
|
<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">
|
<div class="row g-4">
|
||||||
<!-- Chat Model -->
|
<!-- Chat Model -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -108,11 +108,6 @@ function wp_agentic_writer_init() {
|
|||||||
WP_Agentic_Writer_Admin_Columns::get_instance();
|
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' );
|
add_action( 'plugins_loaded', 'wp_agentic_writer_init' );
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user