5 Commits

Author SHA1 Message Date
Dwindi Ramadhana
379a72e52d fix: writing stuck - handle empty model response + no-divider fallback + timeline cleanup
Root causes of writing getting stuck:
1. Model returns empty response for a section → now detected early with
   actionable error message including model name
2. Model responds but without ~~~ARTICLE~~~ divider (happens with fallback
   models like Gemini) → now treats entire response as markdown content
3. Stream ends without 'complete' event (error/exit in PHP) → JS timeline
   entries lingered as 'active' forever. Now deactivated on stream close.
4. Error messages in execution flow now use structured formatAiErrorMessage
   with retry button instead of raw text

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

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

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

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

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

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

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

23
TASKLIST_AUDIT_FIXES.md Normal file
View File

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

View File

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

View File

@@ -33,7 +33,6 @@
.interface-complementary-area__fill:has(#wp-agentic-writer\:wp-agentic-writer),
#wp-agentic-writer\:wp-agentic-writer svg {
margin-bottom: -3px;
width: 18px;
}
.components-tooltip img {
@@ -145,10 +144,10 @@
display: flex;
flex-direction: column;
overflow: hidden;
background: #ffffff;
background: #1a1d23;
border-radius: 0;
min-height: 0;
border: 1px solid #dcdcde;
border: 1px solid #2d3139;
}
.wpaw-messages-inner {
@@ -164,24 +163,24 @@
}
.wpaw-messages-inner::-webkit-scrollbar-track {
background: #f0f0f1;
background: #1a1d23;
border-radius: 0;
}
.wpaw-messages-inner::-webkit-scrollbar-thumb {
background: #c3c4c7;
border-radius: 0;
background: #3d4450;
border-radius: 3px;
}
.wpaw-messages-inner::-webkit-scrollbar-thumb:hover {
background: #8c8f94;
background: #525b6b;
}
.wpaw-input-area {
background: #f6f7f7;
background: #1e2128;
padding: 12px;
border-radius: 0;
border: 1px solid #dcdcde;
border: 1px solid #2d3139;
border-top: none;
margin-top: auto;
}
@@ -195,7 +194,7 @@
.wpaw-input-label {
font-size: 12px;
color: #5f6b7a;
color: #8b95a5;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
@@ -203,24 +202,24 @@
.wpaw-mode-select {
padding: 6px 8px;
border-radius: 2px;
border: 1px solid #8c8f94;
background: #fff;
border-radius: 6px;
border: 1px solid #3d4450;
background: #252830;
font-size: 13px;
font-weight: 400;
color: #1d2227;
color: #c8cdd5;
cursor: pointer;
transition: border-color 0.1s ease;
}
.wpaw-mode-select:hover {
border-color: #2271b1;
border-color: #5b8def;
}
.wpaw-mode-select:focus {
outline: none;
border-color: #2271b1;
box-shadow: 0 0 0 1px #2271b1;
border-color: #5b8def;
box-shadow: 0 0 0 1px #5b8def;
}
#agentMode {
@@ -240,9 +239,10 @@
.wpaw-message {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 0;
background: #fff;
border: 1px solid #dcdcde;
border-radius: 8px;
background: #252830;
border: 1px solid #2d3139;
color: #e0e4ea;
animation: messageSlide 0.2s ease;
}
@@ -259,32 +259,71 @@
}
.wpaw-message-user {
background: #fff;
border-left: 3px solid #2271b1;
background: #2a3040;
border-left: none;
margin-left: 0;
max-width: 80%;
margin-left: auto;
border-radius: 8px;
border: 1px solid #4c4c4c;
}
.dark-theme .wpaw-message-user {
background: #252830;
border-radius: 12px 12px 4px 12px;
border: 1px solid #3b4560;
color: #e8ecf2;
}
.wpaw-message-error {
background: rgb(214, 54, 56, 0.05);
/* border-left: 3px solid #d63638; */
/* border-color: #8a1e1e; */
border: unset;
color: #d63638;
background: rgba(220, 38, 38, 0.08);
border: 1px solid rgba(220, 38, 38, 0.25);
border-left: 3px solid #ef4444;
border-radius: 8px;
color: #fca5a5;
padding: 12px 14px;
}
.wpaw-message-error .wpaw-error-title {
font-weight: 600;
font-size: 13px;
color: #fca5a5;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.wpaw-message-error .wpaw-error-detail {
font-size: 12px;
color: #d4a0a0;
line-height: 1.5;
margin-top: 6px;
}
.wpaw-message-error details {
margin-top: 8px;
}
.wpaw-message-error details summary {
font-size: 11px;
color: #e87171;
cursor: pointer;
user-select: none;
}
.wpaw-message-error details[open] summary {
margin-bottom: 6px;
}
.wpaw-message-error button.is-secondary {
background: #8a1e1e;
color: white;
border: unset !important;
box-shadow: unset !important;
background: rgba(220, 38, 38, 0.15);
color: #fca5a5;
border: 1px solid rgba(220, 38, 38, 0.4) !important;
box-shadow: none !important;
border-radius: 6px;
margin-top: 8px;
font-size: 12px;
padding: 6px 14px;
transition: background 0.15s;
}
.wpaw-message-error button.is-secondary:hover {
background: rgba(220, 38, 38, 0.25);
}
/* Research message styling */
@@ -385,39 +424,48 @@
.wpaw-plan-card,
.wpaw-edit-plan {
border: 2px dashed #4c4c4c;
padding: 12px;
border-radius: 0;
background: #1e2530;
border: 1px solid #2d3a4a;
padding: 14px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.wpaw-plan-card:hover {
border-color: #8c8f94;
border-color: #3d5070;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.wpaw-plan-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
color: #e8ecf2;
}
.wpaw-plan-config-summary {
margin-bottom: 12px;
padding: 8px 10px;
background: #1a1a1a;
border: 1px solid #3c3c3c;
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
.wpaw-plan-section-status {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 2px 8px;
border-radius: 10px;
margin-top: 2px;
font-weight: 600;
}
.wpaw-config-summary-item {
color: #b0b0b0;
font-family: ui-monospace, monospace;
margin-bottom: 4px;
.wpaw-plan-section.pending .wpaw-plan-section-status {
color: #94a3b8;
background: rgba(148, 163, 184, 0.1);
}
.wpaw-config-summary-item:last-child {
margin-bottom: 0;
.wpaw-plan-section.done .wpaw-plan-section-status {
color: #4ade80;
background: rgba(74, 222, 128, 0.1);
}
.wpaw-plan-section.in_progress .wpaw-plan-section-status {
color: #60a5fa;
background: rgba(96, 165, 250, 0.1);
}
.wpaw-plan-sections,
@@ -450,25 +498,6 @@ input.wpaw-plan-section-check:checked::before {
flex: 1;
}
.wpaw-plan-section-title {
font-weight: 600;
margin-bottom: 4px;
}
.wpaw-plan-section-desc {
color: #6c6c6c;
font-size: 13px;
line-height: 1.5;
}
.wpaw-plan-section-status {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #94a3b8;
margin-top: 2px;
}
.wpaw-clear-context {
margin-left: auto;
background: #f6f7f7;
@@ -494,21 +523,113 @@ input.wpaw-plan-section-check:checked::before {
margin-bottom: 10px;
}
.wpaw-refinement-lock-banner {
background: #dbeafe;
color: #1e3a8a;
padding: 8px 10px;
border: 1px solid #93c5fd;
font-size: 12px;
margin-bottom: 10px;
}
.wpaw-refine-confirm-overlay {
position: absolute;
inset: 0;
z-index: 1200;
background: rgba(10, 16, 27, 0.72);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.wpaw-refine-confirm-modal {
width: 100%;
max-width: 420px;
background: #111827;
color: #e5e7eb;
border: 1px solid #334155;
border-radius: 8px;
padding: 16px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
}
.wpaw-refine-confirm-title {
font-size: 15px;
font-weight: 700;
margin-bottom: 8px;
}
.wpaw-refine-confirm-body {
font-size: 13px;
line-height: 1.5;
color: #cbd5e1;
margin-bottom: 12px;
}
.wpaw-refine-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 10px;
}
.wpaw-block-refining {
position: relative;
outline: 2px dashed #3b82f6;
outline-offset: 2px;
}
.wpaw-block-refining::before {
content: 'REFINING';
position: absolute;
top: -12px;
right: 8px;
background: #2563eb;
color: #fff;
font-size: 10px;
line-height: 1;
padding: 3px 6px;
border-radius: 4px;
z-index: 20;
letter-spacing: 0;
font-weight: 700;
}
/* Refinement lock: prevent content editing while still allowing page scroll */
.wpaw-refining-locked .editor-styles-wrapper [contenteditable="true"],
.wpaw-refining-locked .block-editor-rich-text__editable,
.wpaw-refining-locked .block-editor-writing-flow [role="textbox"] {
pointer-events: none !important;
user-select: none !important;
caret-color: transparent !important;
}
.wpaw-refining-locked .block-editor-block-toolbar,
.wpaw-refining-locked .block-editor-default-block-appender,
.wpaw-refining-locked .editor-block-list-item__inline-menu,
.wpaw-refining-locked .block-editor-inserter {
pointer-events: none !important;
opacity: 0.5;
}
.wpaw-ai-response pre {
background: #f1f5f9;
padding: 10px;
background: #1a1d23;
padding: 12px;
border-radius: 8px;
overflow-x: auto;
font-family: "Courier New", monospace;
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-size: 12px;
border: 1px solid #2d3139;
color: #c8cdd5;
}
.wpaw-ai-response code {
background: #e2e8f0;
color: #1f2937;
padding: 2px 4px;
background: #2d3139;
color: #a5d6ff;
padding: 2px 5px;
border-radius: 4px;
font-family: "Courier New", monospace;
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-size: 12px;
}
@@ -525,14 +646,6 @@ input.wpaw-plan-section-check:checked::before {
pointer-events: all;
}
.wpaw-plan-section.done .wpaw-plan-section-status {
color: #15803d;
}
.wpaw-plan-section.in_progress .wpaw-plan-section-status {
color: #2563eb;
}
/* Outline Version Tracking & Inline Editing */
.wpaw-plan-header {
display: flex;
@@ -768,12 +881,12 @@ input.wpaw-plan-section-check:checked::before {
.wpaw-response {
margin: 0 0 12px 0;
border-left: 2px solid #e0e6ed;
color: #1f2937;
border-left: 2px solid #3d4450;
color: #dce0e8;
}
.dark-theme .wpaw-response {
color: #cecece;
color: #dce0e8;
}
.wpaw-ai-response .wpaw-response {
@@ -854,70 +967,6 @@ input.wpaw-plan-section-check:checked::before {
}
}
.wpaw-response-content {
line-height: 1.6;
word-wrap: break-word;
white-space: normal;
}
.wpaw-response-content>* {
padding: 1rem;
}
.wpaw-response-content p {
margin: 0 0 8px;
}
.wpaw-response-content p:last-child {
margin-bottom: 0;
}
.wpaw-response-content h1,
.wpaw-response-content h2,
.wpaw-response-content h3,
.wpaw-response-content h4,
.wpaw-response-content h5,
.wpaw-response-content h6 {
margin: 12px 0 6px;
line-height: 1.4;
}
.wpaw-response-content ul,
.wpaw-response-content ol {
margin: 6px 0 10px 18px;
padding: 0;
}
.wpaw-response-content ul li {
list-style: square;
}
.wpaw-response-content li {
margin: 4px 0;
}
.wpaw-response-content li p {
margin: 4px 0 6px;
}
.dark-theme .wpaw-response-content *:is(h1, h2, h3, h4, h5, h6) {
color: #cecece;
font-weight: bold;
}
.wpaw-response-content table {
border: 1px solid;
border-collapse: collapse;
margin-bottom: 10px;
}
.wpaw-response-content table th,
.wpaw-response-content table td {
border: 1px solid;
padding: 5px;
text-align: left;
}
/* Timeline Progress */
.wpaw-timeline-entry {
display: flex;
@@ -1044,19 +1093,19 @@ input.wpaw-plan-section-check:checked::before {
.wpaw-timeline-content {
flex: 1;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
color: #334155;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
color: #c8cdd5;
}
.wpaw-timeline-message {
font-size: 12px;
color: #334155;
font-size: 12.5px;
color: #c8cdd5;
margin-bottom: 5px;
line-height: 1.5;
}
.dark-theme .wpaw-timeline-message {
color: white;
color: #e0e4ea;
}
.wpaw-timeline-complete {
@@ -1384,6 +1433,7 @@ input.wpaw-plan-section-check:checked::before {
border-bottom: 1px solid #3c3c3c;
font-size: 11px;
text-transform: uppercase;
background-color: #2c2c2c;
}
.wpaw-cost-table td {
@@ -3537,13 +3587,12 @@ input.wpaw-plan-section-check:checked::before {
.wpaw-welcome-icon {
display: block;
margin-bottom: 1rem;
color: #2271b1;
}
.wpaw-welcome-icon svg {
width: 48px;
height: 48px;
width: 64px;
height: 64px;
}
.wpaw-welcome-title {
@@ -3619,6 +3668,23 @@ input.wpaw-plan-section-check:checked::before {
color: #2271b1;
}
.wpaw-session-list {
max-height: 35vh;
overflow-y: auto;
padding-right: 2px;
margin-bottom: 8px;
}
.wpaw-session-open-btn {
display: block;
min-width: 0;
padding: 0;
}
.wpaw-session-open-btn:disabled {
opacity: 0.65;
}
.wpaw-welcome-start-btn {
width: 100%;
padding: 12px 24px !important;
@@ -4483,3 +4549,263 @@ input.wpaw-plan-section-check:checked::before {
.wpaw-provider-info:has(.wpaw-fallback) {
color: #f59e0b;
}
/* ===========================
AUDIT FIXES: Mode Indicator Badge
=========================== */
/* Override WordPress editor-sidebar h3 shrinkage inside our panel */
#wp-agentic-writer\:wp-agentic-writer .interface-complementary-area h3,
#wp-agentic-writer\:wp-agentic-writer h3,
.wpaw-response-content h3,
.wpaw-messages-inner h3 {
font-size: 15px !important;
text-transform: none !important;
font-weight: 700 !important;
color: #e0e4ea !important;
margin-bottom: 0.5em !important;
letter-spacing: normal !important;
}
.wpaw-response-content h2 {
font-size: 17px !important;
color: #e8ecf2 !important;
}
.wpaw-response-content h4,
.wpaw-response-content h5,
.wpaw-response-content h6 {
font-size: 13px !important;
color: #d0d5dd !important;
}
.wpaw-mode-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 8px;
}
.wpaw-mode-badge.mode-chat {
background: rgba(96, 165, 250, 0.12);
color: #60a5fa;
border: 1px solid rgba(96, 165, 250, 0.25);
}
.wpaw-mode-badge.mode-planning {
background: rgba(251, 191, 36, 0.12);
color: #fbbf24;
border: 1px solid rgba(251, 191, 36, 0.25);
}
.wpaw-mode-badge.mode-writing {
background: rgba(74, 222, 128, 0.12);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.25);
}
/* ===========================
AUDIT FIXES: Streaming Heartbeat
=========================== */
.wpaw-heartbeat-notice {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin: 8px 0;
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.2);
border-radius: 8px;
font-size: 12px;
color: #fbbf24;
animation: fadeIn 0.3s ease;
}
.wpaw-heartbeat-notice .wpaw-heartbeat-icon {
animation: pulse-ring 2s infinite;
}
/* ===========================
AUDIT FIXES: Slash Command Hint
=========================== */
.wpaw-input-hint {
position: absolute;
bottom: 100%;
left: 12px;
right: 12px;
padding: 6px 10px;
background: #252830;
border: 1px solid #3d4450;
border-bottom: none;
border-radius: 8px 8px 0 0;
font-size: 11px;
color: #6b7a8d;
display: flex;
align-items: center;
gap: 6px;
}
.wpaw-input-hint kbd {
background: #3d4450;
color: #a0aec0;
padding: 1px 5px;
border-radius: 3px;
font-size: 10px;
font-family: inherit;
}
/* ===========================
AUDIT FIXES: Provider Fallback Warning
=========================== */
.wpaw-provider-warning {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
margin: 8px 0;
background: rgba(251, 146, 60, 0.08);
border: 1px solid rgba(251, 146, 60, 0.2);
border-left: 3px solid #fb923c;
border-radius: 8px;
font-size: 12px;
color: #fdba74;
line-height: 1.5;
}
.wpaw-provider-warning a {
color: #fb923c;
text-decoration: underline;
}
/* ===========================
AUDIT FIXES: DB Health Notice
=========================== */
.wpaw-health-notice {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
margin: 8px 0;
background: rgba(220, 38, 38, 0.06);
border: 1px solid rgba(220, 38, 38, 0.2);
border-radius: 8px;
font-size: 12px;
color: #fca5a5;
}
.wpaw-health-notice a {
color: #ef4444;
text-decoration: underline;
}
/* ===========================
AUDIT FIXES: Confirm Modal for Writing
=========================== */
.wpaw-write-confirm-overlay {
position: absolute;
inset: 0;
z-index: 1200;
background: rgba(10, 16, 27, 0.75);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
animation: fadeIn 0.2s ease;
}
.wpaw-write-confirm-modal {
width: 100%;
max-width: 380px;
background: #1e2530;
color: #e5e7eb;
border: 1px solid #334155;
border-radius: 12px;
padding: 20px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
}
.wpaw-write-confirm-title {
font-size: 15px;
font-weight: 700;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.wpaw-write-confirm-body {
font-size: 13px;
line-height: 1.6;
color: #94a3b8;
margin-bottom: 16px;
}
.wpaw-write-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* ===========================
AUDIT FIXES: Response content dark theme
=========================== */
.wpaw-response-content {
line-height: 1.6;
word-wrap: break-word;
white-space: normal;
color: #dce0e8;
}
.wpaw-response-content>* {
padding: 1rem;
}
.wpaw-response-content p {
margin: 0 0 8px;
}
.wpaw-response-content p:last-child {
margin-bottom: 0;
}
/* Plan section title in dark theme */
.wpaw-plan-section-title {
font-weight: 600;
margin-bottom: 4px;
color: #e0e4ea;
}
.wpaw-plan-section-desc {
color: #8b95a5;
font-size: 13px;
line-height: 1.5;
}
/* Config summary dark */
.wpaw-plan-config-summary {
margin-bottom: 12px;
padding: 10px 12px;
background: #161a20;
border: 1px solid #2d3a4a;
border-radius: 8px;
font-size: 12px;
line-height: 1.6;
}
.wpaw-config-summary-item {
color: #9aa5b4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
margin-bottom: 4px;
font-size: 11.5px;
}
.wpaw-config-summary-item:last-child {
margin-bottom: 0;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,530 @@
<?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 = array() ) {
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
$saved_context = ! empty( $session_id )
? $context_service->get_context( $session_id, $post_id )
: array();
$session_context = $saved_context['context'] ?? array();
$messages = $saved_context['messages'] ?? array();
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
);
return array(
'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' => array(
'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,
),
);
}
/**
* 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 = array() ) {
$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 array(
'message' => null,
'audit' => $package['audit'],
);
}
return array(
'message' => array(
'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']
: array();
return array(
'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 array();
}
/**
* 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 = array();
foreach ( (array) $messages as $message ) {
$role = isset( $message['role'] ) ? (string) $message['role'] : '';
if ( ! in_array( $role, array( 'user', 'assistant' ), true ) ) {
continue;
}
$content = isset( $message['content'] ) ? trim( wp_strip_all_tags( (string) $message['content'] ) ) : '';
if ( '' === $content ) {
continue;
}
$prepared[] = array(
'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'] ?? array();
if ( ! empty( $request_params['postConfig'] ) && is_array( $request_params['postConfig'] ) ) {
$config = wp_parse_args( $request_params['postConfig'], $config );
}
return is_array( $config ) ? $config : array();
}
/**
* 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.
* @return string Context text.
*/
private function build_working_context( $task, $session_context, $recent_messages, $plan, $post_config, $request_params ) {
$sections = array();
$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 );
$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 = array();
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 = array();
$keys = array(
'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 = array();
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 = array();
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 = array(
'activeContent',
'blockContent',
'selectedText',
'sectionContent',
'articleContent',
);
$lines = array();
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 = array();
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 = array();
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 );
}
/**
* Truncate text safely.
*
* @param string $text Text.
* @param int $limit Character limit.
* @return string Truncated text.
*/
private function truncate_text( $text, $limit ) {
$text = trim( (string) $text );
if ( strlen( $text ) <= $limit ) {
return $text;
}
return substr( $text, 0, $limit ) . '...';
}
/**
* Estimate tokens from character length.
*
* @param string $text Text.
* @return int Estimated tokens.
*/
private function estimate_tokens( $text ) {
return (int) ceil( strlen( (string) $text ) / 4 );
}
}

View File

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

View File

@@ -539,7 +539,7 @@ class WP_Agentic_Writer_Conversation_Manager {
$sessions = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$this->table_name} WHERE post_id = %d ORDER BY updated_at DESC",
"SELECT *, JSON_LENGTH(messages) as message_count FROM {$this->table_name} WHERE post_id = %d ORDER BY updated_at DESC",
$post_id
),
ARRAY_A

File diff suppressed because it is too large Load Diff

View File

@@ -342,32 +342,44 @@ class WP_Agentic_Writer_Local_Backend_Provider implements WP_Agentic_Writer_AI_P
);
}
// Test /ping endpoint
$ping_response = wp_remote_get(
$this->base_url . '/ping',
// Best-effort reachability checks. Do not hard-fail here; inference test below is authoritative.
$reachable = false;
$health_endpoints = array( '/ping', '/health', '/' );
foreach ( $health_endpoints as $endpoint ) {
$health_response = wp_remote_get(
$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()
)
);
if ( is_wp_error( $health_response ) ) {
continue;
}
$ping_body = wp_remote_retrieve_body( $ping_response );
if ( 'pong' !== $ping_body ) {
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
@@ -393,6 +405,17 @@ class WP_Agentic_Writer_Local_Backend_Provider implements WP_Agentic_Writer_AI_P
);
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(
'inference_failed',
sprintf(

View File

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

View File

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

View File

@@ -640,6 +640,7 @@ class WP_Agentic_Writer_Settings_V2 {
'monthly' => '0.0000',
'today' => '0.0000',
'avg_per_post' => '0.0000',
'action_summary' => array(),
),
'filters' => array(
'models' => array(),
@@ -662,8 +663,8 @@ class WP_Agentic_Writer_Settings_V2 {
$filter_date_from = isset( $_POST['filter_date_from'] ) ? sanitize_text_field( $_POST['filter_date_from'] ) : '';
$filter_date_to = isset( $_POST['filter_date_to'] ) ? sanitize_text_field( $_POST['filter_date_to'] ) : '';
// Build WHERE clause
$where = array( '1=1' );
// Build WHERE clause (OpenRouter-only for this OpenRouter cost log screen).
$where = array( "provider = 'openrouter'" );
if ( $filter_post > 0 ) {
$where[] = $wpdb->prepare( 'post_id = %d', $filter_post );
}
@@ -697,7 +698,7 @@ class WP_Agentic_Writer_Settings_V2 {
FROM {$table_name}
WHERE {$where_clause}
GROUP BY post_id
ORDER BY total_cost DESC
ORDER BY post_id DESC
LIMIT %d OFFSET %d",
$per_page,
$offset
@@ -737,25 +738,80 @@ class WP_Agentic_Writer_Settings_V2 {
);
}
// Get details for visible posts only (on-demand loading).
// For better performance, we skip details here and let frontend request them.
// The details can be loaded via a separate endpoint when user expands a row.
// Load detail rows for visible posts.
// This keeps expand/collapse usable without requiring a second endpoint.
if ( ! empty( $post_ids ) ) {
$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
$details_sql = $wpdb->prepare(
"SELECT post_id, created_at, model, action, input_tokens, output_tokens, cost
FROM {$table_name}
WHERE provider = 'openrouter' AND post_id IN ({$placeholders})
ORDER BY created_at DESC",
...$post_ids
);
$detail_rows = $wpdb->get_results( $details_sql, ARRAY_A );
$detail_map = array();
foreach ( $detail_rows as $detail_row ) {
$pid = (int) ( $detail_row['post_id'] ?? 0 );
if ( ! isset( $detail_map[ $pid ] ) ) {
$detail_map[ $pid ] = array();
}
$detail_map[ $pid ][] = array(
'created_at' => date_i18n( 'Y-m-d H:i:s', strtotime( $detail_row['created_at'] ) ),
'model' => (string) ( $detail_row['model'] ?? '' ),
'action' => (string) ( $detail_row['action'] ?? '' ),
'input_tokens' => (int) ( $detail_row['input_tokens'] ?? 0 ),
'output_tokens' => (int) ( $detail_row['output_tokens'] ?? 0 ),
'cost' => number_format( (float) ( $detail_row['cost'] ?? 0 ), 4 ),
);
}
foreach ( $formatted_records as $idx => $formatted_record ) {
$pid = (int) ( $formatted_record['post_id'] ?? 0 );
$formatted_records[ $idx ]['details'] = $detail_map[ $pid ] ?? array();
$formatted_records[ $idx ]['details_total'] = count( $formatted_records[ $idx ]['details'] );
}
}
// Get summary stats (all-time aggregation in SQL)
$total_all_time = $wpdb->get_var( "SELECT COALESCE(SUM(cost), 0) FROM {$table_name}" );
$monthly_total = $cost_tracker->get_monthly_total();
$total_all_time = $wpdb->get_var( "SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter'" );
$month_start = date( 'Y-m-01 00:00:00' );
$monthly_total = $wpdb->get_var(
$wpdb->prepare(
"SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter' AND created_at >= %s",
$month_start
)
);
$today_total = $wpdb->get_var(
$wpdb->prepare(
"SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE DATE(created_at) = %s",
"SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter' AND DATE(created_at) = %s",
current_time( 'Y-m-d' )
)
);
$total_posts = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE post_id > 0" );
$total_posts = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE provider = 'openrouter' AND post_id > 0" );
$avg_per_post = $total_posts > 0 ? $total_all_time / $total_posts : 0;
$action_summary_rows = $wpdb->get_results(
"SELECT action, COUNT(*) AS calls, COALESCE(SUM(cost), 0) AS total_cost, COALESCE(AVG(cost), 0) AS avg_cost
FROM {$table_name}
WHERE provider = 'openrouter'
GROUP BY action
ORDER BY total_cost DESC",
ARRAY_A
);
$action_summary = array();
foreach ( $action_summary_rows as $row ) {
$action_summary[] = array(
'action' => (string) ( $row['action'] ?? '' ),
'calls' => (int) ( $row['calls'] ?? 0 ),
'total' => number_format( (float) ( $row['total_cost'] ?? 0 ), 4 ),
'average' => number_format( (float) ( $row['avg_cost'] ?? 0 ), 4 ),
);
}
// Get filter options (distinct values from DB)
$models = $wpdb->get_col( "SELECT DISTINCT model FROM {$table_name} ORDER BY model LIMIT 100" );
$types = $wpdb->get_col( "SELECT DISTINCT action FROM {$table_name} ORDER BY action" );
$models = $wpdb->get_col( "SELECT DISTINCT model FROM {$table_name} WHERE provider = 'openrouter' ORDER BY model LIMIT 100" );
$types = $wpdb->get_col( "SELECT DISTINCT action FROM {$table_name} WHERE provider = 'openrouter' ORDER BY action" );
wp_send_json_success( array(
'records' => $formatted_records,
@@ -768,6 +824,7 @@ class WP_Agentic_Writer_Settings_V2 {
'monthly' => number_format( (float) $monthly_total, 4 ),
'today' => number_format( (float) $today_total, 4 ),
'avg_per_post' => number_format( (float) $avg_per_post, 4 ),
'action_summary' => $action_summary,
),
'filters' => array(
'models' => $models,
@@ -1042,6 +1099,13 @@ class WP_Agentic_Writer_Settings_V2 {
$sanitized['cost_tracking_enabled'] = isset( $input['cost_tracking_enabled'] ) && '1' === $input['cost_tracking_enabled'];
$sanitized['enable_clarification_quiz'] = isset( $input['enable_clarification_quiz'] ) && '1' === $input['enable_clarification_quiz'];
$sanitized['enable_faq_schema'] = isset( $input['enable_faq_schema'] ) ? '1' === $input['enable_faq_schema'] : false;
$sanitized['allow_openrouter_fallback'] = isset( $input['allow_openrouter_fallback'] ) && '1' === $input['allow_openrouter_fallback'];
$sanitized['openrouter_provider_routing_enabled'] = isset( $input['openrouter_provider_routing_enabled'] ) && '1' === $input['openrouter_provider_routing_enabled'];
$sanitized['openrouter_provider_only'] = isset( $input['openrouter_provider_only'] ) && '1' === $input['openrouter_provider_only'];
$sanitized['openrouter_allow_provider_fallbacks'] = isset( $input['openrouter_allow_provider_fallbacks'] ) && '1' === $input['openrouter_allow_provider_fallbacks'];
$provider_slug = isset( $input['openrouter_provider_slug'] ) ? sanitize_key( $input['openrouter_provider_slug'] ) : 'auto';
$sanitized['openrouter_provider_slug'] = '' !== $provider_slug ? $provider_slug : 'auto';
// Sanitize search options
$sanitized['search_engine'] = in_array( $input['search_engine'] ?? '', array( 'auto', 'native', 'exa' ), true )
@@ -1178,6 +1242,11 @@ class WP_Agentic_Writer_Settings_V2 {
$local_backend_key = $settings['local_backend_key'] ?? 'dummy';
$local_backend_model = $settings['local_backend_model'] ?? 'claude-local';
$task_providers = $settings['task_providers'] ?? array();
$allow_openrouter_fallback = ! empty( $settings['allow_openrouter_fallback'] );
$openrouter_provider_routing_enabled = ! empty( $settings['openrouter_provider_routing_enabled'] );
$openrouter_provider_slug = $settings['openrouter_provider_slug'] ?? 'auto';
$openrouter_provider_only = ! empty( $settings['openrouter_provider_only'] );
$openrouter_allow_provider_fallbacks = ! empty( $settings['openrouter_allow_provider_fallbacks'] );
// Get cost tracking data
$cost_tracker = WP_Agentic_Writer_Cost_Tracker::get_instance();
@@ -1214,6 +1283,11 @@ class WP_Agentic_Writer_Settings_V2 {
'local_backend_key',
'local_backend_model',
'task_providers',
'allow_openrouter_fallback',
'openrouter_provider_routing_enabled',
'openrouter_provider_slug',
'openrouter_provider_only',
'openrouter_allow_provider_fallbacks',
'settings'
);
}

View File

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

View File

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

View File

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

View File

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

View File

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