Compare commits
5 Commits
main
...
fix/ux-aud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379a72e52d | ||
|
|
23a34b3035 | ||
|
|
b4ea9025b1 | ||
|
|
f7bf1f5153 | ||
|
|
ae70e4aea9 |
23
TASKLIST_AUDIT_FIXES.md
Normal file
23
TASKLIST_AUDIT_FIXES.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Audit Fix Tasklist
|
||||
|
||||
## Phase 1: UI Theme Consistency & Polish
|
||||
- [x] 1.1 Make chat messages use dark theme consistently (remove white bg)
|
||||
- [x] 1.2 Restyle plan cards (remove dashed wireframe look, add fills/icons/status colors)
|
||||
- [x] 1.3 Fix timeline entry typography (remove monospace, use humanist font)
|
||||
- [x] 1.4 Structure error messages (icon + title + collapsible detail + action)
|
||||
- [x] 1.5 Polish input area cohesion (unify focus bar + mode + textarea)
|
||||
|
||||
## Phase 2: UX Flow Improvements
|
||||
- [x] 2.1 Add contextual placeholder text per agent mode in textarea
|
||||
- [x] 2.2 Add visual mode indicator badge in chat area
|
||||
- [x] 2.3 Simplify welcome screen (reduce session list noise)
|
||||
- [x] 2.4 Add slash command discovery hint in empty input
|
||||
- [x] 2.5 Add confirmation before writing over existing content
|
||||
- [x] 2.6 Add streaming timeout heartbeat (30s no-data reassurance)
|
||||
|
||||
## Phase 3: Error Handling Hardening
|
||||
- [x] 3.1 Add DB table health check on sidebar init
|
||||
- [x] 3.2 Improve "no API key" error with settings link
|
||||
- [x] 3.3 Show in-chat warning when provider fallback triggers
|
||||
- [x] 3.4 Auto-fallback to registry fallback model on unavailability
|
||||
- [x] 3.5 Ensure isLoading always resets on all error paths
|
||||
@@ -18,6 +18,10 @@
|
||||
color: var(--wpaw-primary);
|
||||
}
|
||||
|
||||
.form-check.mt-3 input[type=checkbox] {
|
||||
margin-top: .35rem;
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.wpaw-settings-v2-wrap .card {
|
||||
background: transparent !important;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
1566
assets/js/sidebar.js
1566
assets/js/sidebar.js
File diff suppressed because it is too large
Load Diff
421
docs/architecture/OPENROUTER_BYOK_CONTEXT_STREAMING_SPEC.md
Normal file
421
docs/architecture/OPENROUTER_BYOK_CONTEXT_STREAMING_SPEC.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# OpenRouter BYOK Context and Streaming Spec
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Proposed implementation direction
|
||||
**Goal:** Replace local bash/proxy-first text generation with an OpenRouter BYOK-first API path while preserving article continuity and improving streamed editor UX.
|
||||
|
||||
## Decision
|
||||
|
||||
Use OpenRouter as the primary text transport for `chat`, `clarity`, `planning`, `writing`, and `refinement`, with the user's OpenRouter workspace configured for BYOK provider keys.
|
||||
|
||||
The plugin should continue to store conversation and article memory in WordPress. OpenRouter should be treated as a stateless model gateway: it streams model output, returns usage metadata, applies provider routing, and can cache identical responses. It does not own article continuity.
|
||||
|
||||
Local Backend should become optional or legacy. It is useful for experiments, but it should not be the recommended default because it asks users to run local scripts/proxy tooling and creates trust friction.
|
||||
|
||||
## Current Implementation Snapshot
|
||||
|
||||
The current plugin already has most of the foundation:
|
||||
|
||||
- `includes/interface-ai-provider.php` defines `chat()`, `chat_stream()`, `generate_image()`, `is_configured()`, `test_connection()`, and `supports_task_type()`.
|
||||
- `includes/class-provider-manager.php` routes each task through configured providers and already prevents silent OpenRouter spend when fallback is disabled.
|
||||
- `includes/class-openrouter-provider.php` supports non-streaming and streaming chat completions through OpenRouter.
|
||||
- `includes/class-local-backend-provider.php` supports a local proxy at `/v1/messages`, including a cURL streaming parser and plain JSON fallback.
|
||||
- `includes/class-conversation-manager.php` stores sessions in `{$wpdb->prefix}wpaw_conversations` with `messages` and `context` JSON fields.
|
||||
- `includes/class-context-service.php` is already documented as the single source of truth for messages, `_wpaw_plan`, `_wpaw_post_config`, and legacy chat migration.
|
||||
- `includes/class-gutenberg-sidebar.php` exposes the main REST routes: `/chat`, `/generate-plan`, `/revise-plan`, `/execute-article`, `/refine-block`, `/refine-from-chat`, `/summarize-context`, `/detect-intent`, `/writing-state/{post_id}`, and conversation routes.
|
||||
- Cost tracking already records `post_id`, `session_id`, `model`, `provider`, `action`, input tokens, output tokens, cost, and status.
|
||||
|
||||
The main gap is not lack of streaming. The gap is that several routes still accept full `chatHistory` from the browser and inject it into prompts. That makes continuity depend on the browser payload and can re-send too much context.
|
||||
|
||||
## Product Positioning
|
||||
|
||||
Recommended provider settings:
|
||||
|
||||
```php
|
||||
'task_providers' => array(
|
||||
'chat' => 'openrouter',
|
||||
'clarity' => 'openrouter',
|
||||
'planning' => 'openrouter',
|
||||
'writing' => 'openrouter',
|
||||
'refinement' => 'openrouter',
|
||||
'image' => 'openrouter',
|
||||
),
|
||||
'allow_openrouter_fallback' => false,
|
||||
```
|
||||
|
||||
The UI copy should present this as:
|
||||
|
||||
- Connect OpenRouter API key.
|
||||
- Configure BYOK provider keys inside OpenRouter.
|
||||
- Stream directly into WordPress.
|
||||
- Keep all article memory in WordPress.
|
||||
- Local Backend is advanced or legacy.
|
||||
|
||||
OpenRouter BYOK details to reflect in docs:
|
||||
|
||||
- BYOK lets users route requests through their own provider keys while still using OpenRouter's API surface.
|
||||
- BYOK provider keys are encrypted and used for requests routed through the selected provider.
|
||||
- OpenRouter's BYOK fee is documented as 5 percent of the normal OpenRouter model/provider cost, waived for the first 1M BYOK requests per month.
|
||||
- Users can prevent fallback to OpenRouter shared endpoints by enabling the provider key's "Always use for this provider" behavior in OpenRouter.
|
||||
- OpenRouter usage data is returned in normal responses and in the last SSE message for streamed responses.
|
||||
|
||||
Sources:
|
||||
|
||||
- https://openrouter.ai/docs/guides/overview/auth/byok
|
||||
- https://openrouter.ai/docs/cookbook/administration/usage-accounting
|
||||
- https://openrouter.ai/docs/guides/features/response-caching/
|
||||
|
||||
## Continuity Ownership
|
||||
|
||||
Continuity is owned by WordPress, not OpenRouter.
|
||||
|
||||
Persisted state:
|
||||
|
||||
| State | Current storage | Keep or change |
|
||||
| --- | --- | --- |
|
||||
| Conversation messages | `wpaw_conversations.messages` | Keep |
|
||||
| Session context | `wpaw_conversations.context` | Extend |
|
||||
| Article plan | `_wpaw_plan` post meta | Keep |
|
||||
| Post config | `_wpaw_post_config` post meta | Keep |
|
||||
| Writing state | `_wpaw_writing_status`, `_wpaw_current_section`, `_wpaw_sections_written`, `_wpaw_resume_token` | Keep |
|
||||
| Section to block mapping | `_wpaw_section_blocks` | Keep |
|
||||
| Lightweight post memory | `_wpaw_memory` | Extend or migrate into `context` |
|
||||
| Cost and token usage | `wpaw_cost_tracking` | Extend |
|
||||
|
||||
Recommended new session context shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"working_summary": {
|
||||
"text": "The article is about ...",
|
||||
"updated_at": "2026-06-05T10:30:00+07:00",
|
||||
"source_message_count": 14
|
||||
},
|
||||
"decisions": [
|
||||
{
|
||||
"type": "accept",
|
||||
"target": "outline.section.2",
|
||||
"summary": "Keep the practical checklist framing.",
|
||||
"created_at": "2026-06-05T10:31:00+07:00"
|
||||
}
|
||||
],
|
||||
"rejections": [
|
||||
{
|
||||
"target": "outline.section.4",
|
||||
"summary": "Too generic; needs concrete WordPress examples.",
|
||||
"created_at": "2026-06-05T10:32:00+07:00"
|
||||
}
|
||||
],
|
||||
"research_notes": [
|
||||
{
|
||||
"source": "manual",
|
||||
"title": "User supplied constraint",
|
||||
"excerpt": "Avoid local bash instructions in the default UX.",
|
||||
"tags": ["trust", "onboarding"]
|
||||
}
|
||||
],
|
||||
"token_policy": {
|
||||
"max_recent_messages": 6,
|
||||
"max_summary_tokens": 600,
|
||||
"max_research_snippets": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Store this in `wpaw_conversations.context` first. Avoid adding a new custom table until `context` becomes too large or needs relational querying.
|
||||
|
||||
## Context Builder
|
||||
|
||||
Add a dedicated builder instead of assembling continuity inside each REST handler.
|
||||
|
||||
New file:
|
||||
|
||||
```text
|
||||
includes/class-context-builder.php
|
||||
```
|
||||
|
||||
Primary API:
|
||||
|
||||
```php
|
||||
class WP_Agentic_Writer_Context_Builder {
|
||||
public function build_for_task( $task, $session_id, $post_id, $request_params = array() ) {
|
||||
// Returns normalized prompt parts for chat, planning, writing, refinement, SEO.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Return shape:
|
||||
|
||||
```php
|
||||
array(
|
||||
'system_context' => 'Stable task and policy instructions.',
|
||||
'working_context' => 'Compact summary, decisions, plan, selected post config.',
|
||||
'active_content' => 'The exact section/block/article slice being edited.',
|
||||
'research_context' => 'Only relevant excerpts.',
|
||||
'audit' => array(
|
||||
'included_recent_messages' => 6,
|
||||
'included_research_items' => 3,
|
||||
'estimated_input_tokens' => 2200,
|
||||
'used_full_history' => false,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
Context assembly rules:
|
||||
|
||||
- Always include the task system prompt and language instruction.
|
||||
- Always include post config summary: audience, tone, language, article length, SEO fields, web search preference.
|
||||
- Include `_wpaw_plan` for planning, writing, and outline refinement.
|
||||
- Include only the active block or section for block refinement.
|
||||
- Include recent raw messages only up to `max_recent_messages`.
|
||||
- Include `working_summary` when message history is long.
|
||||
- Include decisions and rejections as compact bullet points.
|
||||
- Include post content only when the task requires whole-article awareness, such as final polish or article-wide refinement.
|
||||
- Never trust browser-provided `chatHistory` as authoritative if `sessionId` is available.
|
||||
|
||||
## Endpoint Changes
|
||||
|
||||
### `/chat`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- Receives `messages` from the browser.
|
||||
- Prepends a system prompt.
|
||||
- Streams or returns a chat response.
|
||||
- Persists user and assistant messages.
|
||||
|
||||
Required change:
|
||||
|
||||
- Use browser `messages` only to identify the latest user message.
|
||||
- Load authoritative session context from `WP_Agentic_Writer_Context_Service`.
|
||||
- Build final messages through `WP_Agentic_Writer_Context_Builder`.
|
||||
- Persist the raw user message and assistant response after completion.
|
||||
|
||||
### `/generate-plan`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- Accepts `topic`, `context`, `chatHistory`, and other config.
|
||||
- Serializes full `chatHistory` into the planning prompt.
|
||||
- Stores `_wpaw_plan` and `_wpaw_memory`.
|
||||
|
||||
Required change:
|
||||
|
||||
- Keep `topic`, `context`, `clarificationAnswers`, and `post_config`.
|
||||
- Replace full `chatHistory` injection with a context package from the builder.
|
||||
- Save generated plan to `_wpaw_plan`.
|
||||
- Update `wpaw_conversations.context.working_summary` after plan generation.
|
||||
|
||||
### `/revise-plan`
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Include current `_wpaw_plan`.
|
||||
- Include latest user instruction.
|
||||
- Include accepted/rejected outline decisions.
|
||||
- Ask for raw JSON plan only.
|
||||
- Save previous plan as a version entry inside `wpaw_conversations.context.plan_versions` before overwriting `_wpaw_plan`.
|
||||
|
||||
### `/execute-article`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- Writes sections from the plan.
|
||||
- Streams section content and block events.
|
||||
- Updates `_wpaw_plan` section statuses.
|
||||
|
||||
Required change:
|
||||
|
||||
- For each section, send the section brief, global article summary, relevant decisions, and relevant research.
|
||||
- Do not send the full conversation for every section.
|
||||
- After each section completes, update writing state and append a section summary to session context.
|
||||
|
||||
### `/refine-block` and `/refine-from-chat`
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Send active block content, neighboring heading/section context, relevant plan entry, and latest instruction.
|
||||
- Include compact working summary and decisions.
|
||||
- Do not include the full draft unless the requested operation is article-wide.
|
||||
|
||||
### `/summarize-context`
|
||||
|
||||
Current behavior:
|
||||
|
||||
- Summarizes browser-provided `chatHistory`.
|
||||
- Returns summary but does not appear to be the authoritative persistence mechanism.
|
||||
|
||||
Required change:
|
||||
|
||||
- Accept `sessionId`.
|
||||
- Load authoritative session messages.
|
||||
- Save the resulting summary into `wpaw_conversations.context.working_summary`.
|
||||
- Return `summary`, `message_count`, `source_message_count`, `tokens_saved`, and provider metadata.
|
||||
|
||||
## Streaming Transport
|
||||
|
||||
OpenRouter streaming is already implemented in `WP_Agentic_Writer_OpenRouter_Provider::chat_stream()`.
|
||||
|
||||
Keep this transport shape:
|
||||
|
||||
```php
|
||||
$body = array(
|
||||
'model' => $model,
|
||||
'messages' => $messages,
|
||||
'stream' => true,
|
||||
);
|
||||
```
|
||||
|
||||
Modernize usage handling:
|
||||
|
||||
- OpenRouter now returns full usage metadata automatically.
|
||||
- `usage: { include: true }` and `stream_options: { include_usage: true }` are documented as deprecated and no longer required.
|
||||
- Keep parsing the final `usage` object from streamed chunks.
|
||||
- Extend cost tracking to store cache metadata when available.
|
||||
|
||||
Recommended emitted SSE events:
|
||||
|
||||
```json
|
||||
{"type":"provider","provider":"openrouter","model":"openai/gpt-4o-mini","byok_expected":true}
|
||||
{"type":"conversational_stream","content":"partial accumulated text"}
|
||||
{"type":"usage","input_tokens":1200,"output_tokens":360,"cached_tokens":0,"cost":0.0012}
|
||||
{"type":"complete","session_id":"abc123","totalCost":0.0012}
|
||||
```
|
||||
|
||||
Use the existing browser parsing path in `assets/js/sidebar.js` and add support for the optional `provider` and `usage` event types.
|
||||
|
||||
## Response Caching Policy
|
||||
|
||||
OpenRouter response caching should be used for deterministic, duplicate-safe operations only. It is not article memory.
|
||||
|
||||
Recommended use:
|
||||
|
||||
- `detect_intent`
|
||||
- `summarize_context` retry
|
||||
- connection test
|
||||
- repeated model capability lookups if routed through completion calls
|
||||
|
||||
Avoid by default:
|
||||
|
||||
- article draft generation
|
||||
- outline revision
|
||||
- refinement requests
|
||||
- image prompt generation
|
||||
|
||||
Provider implementation change:
|
||||
|
||||
```php
|
||||
if ( ! empty( $options['openrouter_response_cache'] ) ) {
|
||||
$headers[] = 'X-OpenRouter-Cache: true';
|
||||
$headers[] = 'X-OpenRouter-Cache-TTL: ' . (int) ( $options['openrouter_cache_ttl'] ?? 300 );
|
||||
}
|
||||
```
|
||||
|
||||
Important limitations:
|
||||
|
||||
- Cache hits only happen for identical requests.
|
||||
- Streaming and non-streaming requests are cached separately.
|
||||
- Cache hit usage counters are zeroed.
|
||||
- Response caching is beta and requires OpenRouter to store response data temporarily.
|
||||
|
||||
## Usage and Budget Tracking
|
||||
|
||||
Extend `wpaw_cost_tracking` with optional cache and upstream fields:
|
||||
|
||||
```sql
|
||||
ALTER TABLE {$wpdb->prefix}wpaw_cost_tracking
|
||||
ADD COLUMN cached_tokens int(11) DEFAULT 0 AFTER output_tokens,
|
||||
ADD COLUMN cache_write_tokens int(11) DEFAULT 0 AFTER cached_tokens,
|
||||
ADD COLUMN upstream_inference_cost decimal(10,6) DEFAULT NULL AFTER cost,
|
||||
ADD COLUMN generation_id varchar(64) DEFAULT '' AFTER status;
|
||||
```
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Put this behind a schema version bump, not plugin version alone.
|
||||
- Keep existing `maybe_upgrade_table()` pattern in `WP_Agentic_Writer_Cost_Tracker`.
|
||||
- Parse `usage.prompt_tokens_details.cached_tokens`.
|
||||
- Parse `usage.prompt_tokens_details.cache_write_tokens`.
|
||||
- Parse `usage.cost_details.upstream_inference_cost` for BYOK requests.
|
||||
- Include a monthly token budget view alongside the existing cost view.
|
||||
|
||||
Budget metric examples:
|
||||
|
||||
```php
|
||||
billable_input_tokens = max( 0, input_tokens - cached_tokens );
|
||||
total_monthly_tokens = sum( input_tokens + output_tokens );
|
||||
byok_free_request_counter = count( provider = 'openrouter' and status = 'success' );
|
||||
```
|
||||
|
||||
Note: OpenRouter documents the BYOK waiver as first 1M BYOK requests per month, not first 1M tokens. Keep UI wording precise.
|
||||
|
||||
## Settings UI Changes
|
||||
|
||||
Update Settings V2:
|
||||
|
||||
- Rename default cloud path to `OpenRouter BYOK / API`.
|
||||
- Keep API key storage in `wp_agentic_writer_settings.openrouter_api_key`.
|
||||
- Add a help panel explaining that provider BYOK keys are configured in OpenRouter, not in WordPress.
|
||||
- Add a "Prevent shared fallback" checklist item that links users to OpenRouter BYOK provider settings.
|
||||
- Move Local Backend to an `Advanced` or `Legacy Local Backend` section.
|
||||
- Make provider routing default all text tasks to `openrouter`.
|
||||
- Keep image task on `openrouter`.
|
||||
- Show a trust note: WordPress streams directly to OpenRouter; no local shell or CLI process is required.
|
||||
|
||||
Do not collect provider keys directly in WordPress unless there is a deliberate product decision to bypass OpenRouter BYOK management. The safer default is only storing the OpenRouter API key.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Documentation and defaults
|
||||
|
||||
- Add this spec.
|
||||
- Update user-facing Local Backend docs to say local backend is optional/advanced.
|
||||
- Default new installs to OpenRouter for all tasks.
|
||||
- Keep existing installs unchanged unless the user opts in.
|
||||
|
||||
### Phase 2: Context builder
|
||||
|
||||
- Add `includes/class-context-builder.php`.
|
||||
- Load it from `wp-agentic-writer.php`.
|
||||
- Move repeated context assembly out of `class-gutenberg-sidebar.php`.
|
||||
- Make `/chat`, `/generate-plan`, `/revise-plan`, and refinement endpoints use the builder.
|
||||
|
||||
### Phase 3: Authoritative summaries
|
||||
|
||||
- Extend `WP_Agentic_Writer_Context_Service` with:
|
||||
- `get_session_context( $session_id )`
|
||||
- `update_session_context( $session_id, $patch )`
|
||||
- `summarize_session_if_needed( $session_id, $post_id )`
|
||||
- Make `/summarize-context` persist summaries to `wpaw_conversations.context`.
|
||||
- Store plan versions and section summaries in context.
|
||||
|
||||
### Phase 4: Streaming and usage polish
|
||||
|
||||
- Remove deprecated OpenRouter usage request parameters.
|
||||
- Emit optional `provider` and `usage` SSE events.
|
||||
- Extend cost tracking schema for cached tokens and BYOK upstream cost.
|
||||
- Add UI display for monthly token usage.
|
||||
|
||||
### Phase 5: Local backend repositioning
|
||||
|
||||
- Move local backend downloads and setup UI to advanced/legacy.
|
||||
- Keep `WP_Agentic_Writer_Local_Backend_Provider` for existing users.
|
||||
- Disable automatic local backend recommendation in onboarding.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A new article can be planned and written through OpenRouter streaming without any local bash/proxy setup.
|
||||
- Existing conversation history persists through `wpaw_conversations`.
|
||||
- Plan generation no longer sends full browser `chatHistory` when `sessionId` is available.
|
||||
- Refining a block includes active block, relevant plan, compact decisions, and recent messages, not full raw history.
|
||||
- Streaming responses show partial text in the editor and finish with usage metadata.
|
||||
- Cost tracking records provider, model, action, session, tokens, and cost as it does today.
|
||||
- New cache fields are recorded when OpenRouter returns them.
|
||||
- Local Backend still works for users who already configured it, but it is no longer the default recommendation.
|
||||
|
||||
## Implementation Risks
|
||||
|
||||
- Some existing frontend flows rely on `messages` as the full source of truth. Those flows need to pass `sessionId` reliably before backend context can become authoritative.
|
||||
- `wpaw_conversations.context` is `LONGTEXT`, so it can hold rich JSON, but large contexts should still be summarized to keep admin queries fast.
|
||||
- OpenRouter response caching is beta and should not be presented as durable memory.
|
||||
- BYOK provider fallback behavior is configured in OpenRouter, so the WordPress UI can guide and detect symptoms but cannot fully enforce provider-key policy from this plugin alone.
|
||||
7
docs/user-facing/downloads/.gitignore
vendored
7
docs/user-facing/downloads/.gitignore
vendored
@@ -1,7 +0,0 @@
|
||||
# Ignore node_modules in local backend package
|
||||
agentic-writer-local-backend/node_modules/
|
||||
agentic-writer-local-backend/proxy.log
|
||||
agentic-writer-local-backend/proxy.pid
|
||||
|
||||
# Keep the distributable ZIP
|
||||
!agentic-writer-local-backend.zip
|
||||
Binary file not shown.
@@ -1,170 +0,0 @@
|
||||
# Agentic Writer Local Backend
|
||||
|
||||
Run unlimited AI content generation on your own machine using your Claude CLI + Z.ai/Anthropic account.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure you have:
|
||||
|
||||
- ✅ **Claude CLI** installed and configured
|
||||
- Get it: [https://claude.ai/code](https://claude.ai/code) or [https://z.ai](https://z.ai)
|
||||
- Verify: `claude --version` or `which claude`
|
||||
- ✅ **Node.js 18+** installed
|
||||
- Download: [https://nodejs.org](https://nodejs.org)
|
||||
- Verify: `node --version`
|
||||
- ✅ **Z.ai Coding Plan** or **Anthropic API key** configured in Claude CLI
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Extract Package
|
||||
|
||||
```bash
|
||||
unzip agentic-writer-local-backend.zip
|
||||
cd agentic-writer-local-backend
|
||||
```
|
||||
|
||||
### 2. Start the Proxy
|
||||
|
||||
```bash
|
||||
chmod +x *.sh
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
You'll see:
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════
|
||||
✅ Local Backend Running!
|
||||
═══════════════════════════════════════════════════
|
||||
|
||||
Your Configuration:
|
||||
Base URL: http://192.168.1.105:8080
|
||||
API Key: dummy
|
||||
Model: claude-local
|
||||
```
|
||||
|
||||
### 3. Configure WordPress Plugin
|
||||
|
||||
1. Open **WP Admin** → **Agentic Writer** → **Settings** → **Local Backend**
|
||||
2. Paste the **Base URL** shown above
|
||||
3. API Key: `dummy`
|
||||
4. Click **Test Connection** → should show ✅
|
||||
5. Start generating content!
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
./start-proxy.sh # Start proxy (runs in background)
|
||||
./stop-proxy.sh # Stop proxy
|
||||
./test-connection.sh # Test if proxy responds
|
||||
./get-local-ip.sh # Find your local IP address
|
||||
tail -f proxy.log # View real-time logs
|
||||
```
|
||||
|
||||
## Firewall Setup
|
||||
|
||||
The proxy needs to accept connections from your WordPress site.
|
||||
|
||||
### macOS
|
||||
|
||||
1. **System Settings** → **Network** → **Firewall**
|
||||
2. Click **Options** → **Add** → Select `node`
|
||||
3. Set to **Allow incoming connections**
|
||||
|
||||
### Linux (ufw)
|
||||
|
||||
```bash
|
||||
sudo ufw allow 8080/tcp
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. **Windows Defender Firewall** → **Advanced Settings**
|
||||
2. **Inbound Rules** → **New Rule**
|
||||
3. **Port** → TCP **8080** → **Allow**
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
WordPress Plugin → HTTP POST → Local Proxy (port 8080)
|
||||
↓
|
||||
Spawns Claude CLI
|
||||
↓
|
||||
Returns AI Response
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 🆓 **Free**: Uses your existing Z.ai/Anthropic subscription
|
||||
- 🔒 **Private**: Content never leaves your network
|
||||
- ⚡ **Fast**: LAN latency (~50-200ms)
|
||||
- 🚀 **Unlimited**: No rate limits, no token counting
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed solutions.
|
||||
|
||||
### Quick Fixes
|
||||
|
||||
**"Connection failed" in plugin:**
|
||||
```bash
|
||||
# Check proxy is running
|
||||
ps aux | grep claude-proxy
|
||||
|
||||
# Restart if needed
|
||||
./stop-proxy.sh && ./start-proxy.sh
|
||||
```
|
||||
|
||||
**"Claude CLI not found":**
|
||||
```bash
|
||||
# Verify Claude is installed
|
||||
which claude
|
||||
claude --version
|
||||
|
||||
# Test Claude works
|
||||
echo "Hello" | claude
|
||||
```
|
||||
|
||||
**"Wrong IP address":**
|
||||
```bash
|
||||
# Find your correct IP
|
||||
./get-local-ip.sh
|
||||
|
||||
# Or manually:
|
||||
# macOS: ipconfig getifaddr en0
|
||||
# Linux: ip route get 1 | awk '{print $7}'
|
||||
```
|
||||
|
||||
**Port 8080 already in use:**
|
||||
```bash
|
||||
# Find what's using it
|
||||
lsof -i :8080
|
||||
|
||||
# Change port (edit claude-proxy.js)
|
||||
PORT=9000 node claude-proxy.js
|
||||
# Update plugin Base URL to: http://your-ip:9000
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Proxy binds to `0.0.0.0` (all network interfaces) for LAN access
|
||||
- No authentication by design (LAN trust model)
|
||||
- All request prompts are logged to `proxy.log`
|
||||
- For internet exposure, use ngrok/reverse proxy with authentication
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
PORT=9000 ./start-proxy.sh # Use different port
|
||||
NODE_ENV=production # Production mode
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [Plugin Docs](https://github.com/your/plugin)
|
||||
- **Issues**: [GitHub Issues](https://github.com/your/plugin/issues)
|
||||
- **Community**: [Discord](https://discord.gg/your-server)
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0+ - Same as WP Agentic Writer plugin
|
||||
@@ -1,339 +0,0 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
Common issues and solutions for Agentic Writer Local Backend.
|
||||
|
||||
## Connection Issues
|
||||
|
||||
### "Connection timeout" in Plugin
|
||||
|
||||
**Symptoms:**
|
||||
- Plugin shows "Connection timeout" error
|
||||
- Test connection fails
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check proxy is running:**
|
||||
```bash
|
||||
ps aux | grep claude-proxy
|
||||
```
|
||||
|
||||
2. **Restart proxy:**
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
3. **Check logs:**
|
||||
```bash
|
||||
tail -f proxy.log
|
||||
```
|
||||
|
||||
4. **Verify IP address:**
|
||||
```bash
|
||||
./get-local-ip.sh
|
||||
```
|
||||
|
||||
### "Connection refused"
|
||||
|
||||
**Cause:** Proxy not running or wrong IP
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Start proxy:**
|
||||
```bash
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
2. **Check firewall:**
|
||||
- macOS: System Settings → Network → Firewall → Allow Node.js
|
||||
- Linux: `sudo ufw allow 8080/tcp`
|
||||
- Windows: Defender Firewall → Allow port 8080
|
||||
|
||||
3. **Test locally first:**
|
||||
```bash
|
||||
curl http://localhost:8080/ping
|
||||
# Should return: pong
|
||||
```
|
||||
|
||||
## Claude CLI Issues
|
||||
|
||||
### "Claude CLI not found"
|
||||
|
||||
**Verify installation:**
|
||||
```bash
|
||||
which claude
|
||||
# macOS: /opt/homebrew/bin/claude or /usr/local/bin/claude
|
||||
# Linux: ~/.local/bin/claude or /usr/bin/claude
|
||||
```
|
||||
|
||||
**Fix PATH:**
|
||||
```bash
|
||||
# Add to ~/.zshrc or ~/.bashrc
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**Reinstall Claude CLI:**
|
||||
- Visit: [https://claude.ai/code](https://claude.ai/code)
|
||||
- Follow installation instructions
|
||||
|
||||
### "No response from Claude"
|
||||
|
||||
**Test Claude manually:**
|
||||
```bash
|
||||
echo "Hello, reply with: Test successful" | claude
|
||||
```
|
||||
|
||||
**Check authentication:**
|
||||
```bash
|
||||
claude --version
|
||||
# Should show version and auth status
|
||||
```
|
||||
|
||||
**Reconfigure Claude:**
|
||||
- Check Z.ai account: [https://z.ai](https://z.ai)
|
||||
- Or Anthropic API key setup
|
||||
|
||||
## Network Issues
|
||||
|
||||
### Wrong IP Address Detected
|
||||
|
||||
**Find correct IP:**
|
||||
```bash
|
||||
# macOS
|
||||
ipconfig getifaddr en0 # WiFi
|
||||
ipconfig getifaddr en1 # Ethernet
|
||||
|
||||
# Linux
|
||||
ip route get 1 | awk '{print $7}'
|
||||
hostname -I
|
||||
|
||||
# Windows
|
||||
ipconfig
|
||||
# Look for "IPv4 Address" under active adapter
|
||||
```
|
||||
|
||||
**Update plugin settings:**
|
||||
- Use the correct IP in Base URL: `http://CORRECT-IP:8080`
|
||||
|
||||
### Port 8080 Already in Use
|
||||
|
||||
**Find what's using it:**
|
||||
```bash
|
||||
lsof -i :8080
|
||||
# or
|
||||
netstat -anp | grep 8080
|
||||
```
|
||||
|
||||
**Change port:**
|
||||
|
||||
1. Edit `claude-proxy.js`:
|
||||
```javascript
|
||||
const PORT = process.env.PORT || 9000; // Change 8080 to 9000
|
||||
```
|
||||
|
||||
2. Restart proxy:
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
PORT=9000 ./start-proxy.sh
|
||||
```
|
||||
|
||||
3. Update plugin Base URL: `http://your-ip:9000`
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Response Times
|
||||
|
||||
**Normal latency:**
|
||||
- Local network: 50-200ms
|
||||
- Claude CLI processing: 2-30 seconds depending on prompt
|
||||
|
||||
**If consistently slow:**
|
||||
|
||||
1. **Check network:**
|
||||
```bash
|
||||
ping 192.168.1.105 # Your proxy IP
|
||||
```
|
||||
|
||||
2. **Monitor logs:**
|
||||
```bash
|
||||
tail -f proxy.log
|
||||
```
|
||||
|
||||
3. **Check machine resources:**
|
||||
- CPU usage: Claude CLI is CPU-intensive
|
||||
- Memory: Ensure sufficient RAM available
|
||||
|
||||
### Proxy Crashes
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
cat proxy.log | tail -50
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- Out of memory: Close other applications
|
||||
- Claude CLI timeout: Increase timeout in `claude-proxy.js`
|
||||
- Malformed requests: Check plugin version compatibility
|
||||
|
||||
**Restart with clean state:**
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
rm proxy.log
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
## Plugin Integration Issues
|
||||
|
||||
### "Invalid response format"
|
||||
|
||||
**Cause:** Claude response doesn't match expected JSON format
|
||||
|
||||
**Debug:**
|
||||
1. Check `proxy.log` for actual Claude output
|
||||
2. Test manually:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"messages":[{"role":"user","content":"Hello"}]}'
|
||||
```
|
||||
|
||||
3. Update Claude CLI if outdated:
|
||||
```bash
|
||||
claude --version
|
||||
# Upgrade if needed
|
||||
```
|
||||
|
||||
### Cost Tracking Shows $0
|
||||
|
||||
**Expected behavior:** Local backend is free, plugin should show `$0.00 (Local)`
|
||||
|
||||
**If concerned:**
|
||||
- This is correct - local backend has no API costs
|
||||
- Dashboard should show "X requests local (free)"
|
||||
|
||||
## Advanced Troubleshooting
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Edit `claude-proxy.js`:
|
||||
```javascript
|
||||
const DEBUG = true; // Add at top of file
|
||||
|
||||
// In /v1/messages handler:
|
||||
if (DEBUG) {
|
||||
console.log('Full request:', JSON.stringify(req.body, null, 2));
|
||||
console.log('Full response:', output);
|
||||
}
|
||||
```
|
||||
|
||||
### Test with curl
|
||||
|
||||
**Ping:**
|
||||
```bash
|
||||
curl http://localhost:8080/ping
|
||||
# Expected: pong
|
||||
```
|
||||
|
||||
**Inference:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role": "user", "content": "Reply with: Test successful"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected response:**
|
||||
```json
|
||||
{
|
||||
"id": "local-1234567890",
|
||||
"object": "chat.completion",
|
||||
"model": "claude-local",
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "Test successful"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Permissions Issues (macOS)
|
||||
|
||||
**Make scripts executable:**
|
||||
```bash
|
||||
chmod +x start-proxy.sh stop-proxy.sh test-connection.sh get-local-ip.sh
|
||||
```
|
||||
|
||||
**If "permission denied":**
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la *.sh
|
||||
|
||||
# Reset if needed
|
||||
chmod 755 *.sh
|
||||
```
|
||||
|
||||
## Still Having Issues?
|
||||
|
||||
1. **Check system requirements:**
|
||||
- Node.js 18+: `node --version`
|
||||
- Claude CLI installed: `which claude`
|
||||
- Sufficient disk space: `df -h`
|
||||
|
||||
2. **Collect diagnostic info:**
|
||||
```bash
|
||||
echo "Node version:" $(node --version)
|
||||
echo "Claude path:" $(which claude)
|
||||
echo "Local IP:" $(./get-local-ip.sh)
|
||||
echo "Proxy status:" $(ps aux | grep claude-proxy)
|
||||
tail -20 proxy.log
|
||||
```
|
||||
|
||||
3. **Reset everything:**
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
rm -rf node_modules proxy.log proxy.pid
|
||||
npm install
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
4. **Get help:**
|
||||
- GitHub Issues: [Report Bug](https://github.com/your/plugin/issues)
|
||||
- Discord Community: [Join Chat](https://discord.gg/your-server)
|
||||
- Include: OS, Node version, Claude CLI version, error logs
|
||||
|
||||
## Environment-Specific Notes
|
||||
|
||||
### macOS
|
||||
|
||||
- Default Claude path: `/opt/homebrew/bin/claude`
|
||||
- Firewall: System Settings → Network → Firewall
|
||||
- IP detection: `ipconfig getifaddr en0`
|
||||
|
||||
### Linux
|
||||
|
||||
- Default Claude path: `~/.local/bin/claude`
|
||||
- Firewall: `sudo ufw allow 8080/tcp`
|
||||
- IP detection: `ip route get 1 | awk '{print $7}'`
|
||||
|
||||
### Windows
|
||||
|
||||
- Claude path varies, check `where claude`
|
||||
- Firewall: Windows Defender → Allow port 8080
|
||||
- IP detection: `ipconfig` (look for IPv4)
|
||||
- Scripts: Use Git Bash or WSL to run `.sh` scripts
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **LAN only:** Don't expose proxy to internet without authentication
|
||||
2. **Firewall:** Restrict to specific IPs if on shared network
|
||||
3. **Logs:** `proxy.log` contains all prompts - review periodically
|
||||
4. **Updates:** Keep Node.js and Claude CLI updated
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-02-27
|
||||
**Version:** 1.0.0
|
||||
@@ -1,122 +0,0 @@
|
||||
const express = require('express');
|
||||
const { spawn } = require('child_process');
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/ping', (req, res) => {
|
||||
res.send('pong');
|
||||
});
|
||||
|
||||
// Main inference endpoint (OpenAI-compatible format)
|
||||
app.post('/v1/messages', async (req, res) => {
|
||||
const { messages } = req.body;
|
||||
|
||||
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
message: 'Invalid request: messages array required'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the last user message as the prompt
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const prompt = lastMessage.content;
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Request from:', req.ip);
|
||||
console.log('Prompt length:', prompt.length, 'chars');
|
||||
console.log('Prompt preview:', prompt.substring(0, 150) + '...');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
// Spawn Claude CLI process
|
||||
const claude = spawn('claude', [], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
claude.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
process.stdout.write('.');
|
||||
});
|
||||
|
||||
claude.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
console.error('Claude stderr:', data.toString());
|
||||
});
|
||||
|
||||
claude.on('close', (code) => {
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Claude exit code:', code);
|
||||
console.log('Response length:', output.length, 'chars');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (code !== 0 || !output.trim()) {
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Claude CLI error',
|
||||
details: errorOutput || 'No response from Claude'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return OpenAI-compatible response format
|
||||
res.json({
|
||||
id: 'local-' + Date.now(),
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: 'claude-local',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: output.trim()
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
claude.on('error', (err) => {
|
||||
console.error('Failed to spawn Claude CLI:', err);
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to spawn Claude CLI',
|
||||
details: err.message
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Send prompt to Claude after brief pause
|
||||
setTimeout(() => {
|
||||
claude.stdin.write(prompt + '\n');
|
||||
claude.stdin.end();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
console.log('🚀 Agentic Writer Local Backend Started!');
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
console.log(`Local: http://localhost:${PORT}`);
|
||||
console.log(`Network: http://YOUR-IP:${PORT}`);
|
||||
console.log('');
|
||||
console.log('Plugin Configuration:');
|
||||
console.log(` Base URL: http://YOUR-IP:${PORT}`);
|
||||
console.log(` API Key: dummy`);
|
||||
console.log(` Model: claude-local`);
|
||||
console.log('');
|
||||
console.log('Health check: GET /ping');
|
||||
console.log('Inference: POST /v1/messages');
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Detecting your local IP address..."
|
||||
echo ""
|
||||
|
||||
# Detect local IP based on OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS - try en0 (WiFi) then en1 (Ethernet)
|
||||
IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "")
|
||||
INTERFACE=$(ifconfig en0 &>/dev/null && echo "en0 (WiFi)" || echo "en1 (Ethernet)")
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || hostname -I | awk '{print $1}')
|
||||
INTERFACE="default"
|
||||
else
|
||||
# Windows or unknown
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||||
INTERFACE="unknown"
|
||||
fi
|
||||
|
||||
if [ -z "$IP" ]; then
|
||||
echo "❌ Could not detect IP address automatically"
|
||||
echo ""
|
||||
echo "Manual detection:"
|
||||
echo " macOS: ipconfig getifaddr en0"
|
||||
echo " Linux: ip route get 1 | awk '{print \$7}'"
|
||||
echo " Windows: ipconfig (look for IPv4 Address)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Your local IP: $IP ($INTERFACE)"
|
||||
echo ""
|
||||
echo "Use this in your plugin settings:"
|
||||
echo " Base URL: http://$IP:8080"
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "agentic-writer-local-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Local backend proxy for WP Agentic Writer using Claude CLI",
|
||||
"main": "claude-proxy.js",
|
||||
"scripts": {
|
||||
"start": "node claude-proxy.js",
|
||||
"test": "curl http://localhost:8080/ping"
|
||||
},
|
||||
"keywords": [
|
||||
"wordpress",
|
||||
"ai",
|
||||
"claude",
|
||||
"proxy"
|
||||
],
|
||||
"author": "WP Agentic Writer",
|
||||
"license": "GPL-2.0+",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Starting Agentic Writer Local Backend..."
|
||||
echo ""
|
||||
|
||||
# Check dependencies
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ Node.js not found. Install from https://nodejs.org/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v claude &> /dev/null; then
|
||||
echo "❌ Claude CLI not found. Install and configure first."
|
||||
echo " Check: https://claude.ai/code or https://z.ai"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Auto-install express if needed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Detect local IP
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "127.0.0.1")
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || echo "127.0.0.1")
|
||||
else
|
||||
# Windows/other
|
||||
LOCAL_IP="127.0.0.1"
|
||||
fi
|
||||
|
||||
echo "✅ Dependencies OK"
|
||||
echo "✅ Claude CLI found: $(which claude)"
|
||||
echo ""
|
||||
echo "Starting proxy server..."
|
||||
echo ""
|
||||
|
||||
# Start server in background
|
||||
nohup node claude-proxy.js > proxy.log 2>&1 &
|
||||
PID=$!
|
||||
echo $PID > proxy.pid
|
||||
|
||||
# Wait for server to boot
|
||||
sleep 2
|
||||
|
||||
# Test if running
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
echo "✅ Local Backend Running!"
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Your Configuration:"
|
||||
echo " Base URL: http://$LOCAL_IP:8080"
|
||||
echo " API Key: dummy"
|
||||
echo " Model: claude-local"
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo " 1. Open your WordPress Admin"
|
||||
echo " 2. Go to Agentic Writer → Settings → Local Backend"
|
||||
echo " 3. Paste the Base URL above"
|
||||
echo " 4. Click 'Test Connection'"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Logs: tail -f proxy.log"
|
||||
echo " Stop: ./stop-proxy.sh"
|
||||
echo " Test: ./test-connection.sh"
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
else
|
||||
echo "❌ Failed to start. Check proxy.log for errors."
|
||||
cat proxy.log
|
||||
rm -f proxy.pid
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f proxy.pid ]; then
|
||||
PID=$(cat proxy.pid)
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
kill $PID
|
||||
rm proxy.pid
|
||||
echo "🛑 Local Backend stopped (PID: $PID)"
|
||||
else
|
||||
echo "⚠️ No process found with PID: $PID"
|
||||
rm proxy.pid
|
||||
fi
|
||||
else
|
||||
# Fallback: kill by process name
|
||||
pkill -f claude-proxy.js
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "🛑 Stopped all claude-proxy processes"
|
||||
else
|
||||
echo "ℹ️ No claude-proxy processes running"
|
||||
fi
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Testing local backend connection..."
|
||||
echo ""
|
||||
|
||||
# Test /ping endpoint
|
||||
echo "1. Testing health check..."
|
||||
PING_RESPONSE=$(curl -s http://localhost:8080/ping 2>&1)
|
||||
|
||||
if [ "$PING_RESPONSE" = "pong" ]; then
|
||||
echo " ✅ Health check passed"
|
||||
else
|
||||
echo " ❌ Health check failed"
|
||||
echo " Response: $PING_RESPONSE"
|
||||
echo ""
|
||||
echo "Is the proxy running? Check: ps aux | grep claude-proxy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test /v1/messages endpoint
|
||||
echo "2. Testing inference..."
|
||||
RESPONSE=$(curl -s -X POST http://localhost:8080/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"messages":[{"role":"user","content":"Reply with exactly: Test successful"}]}' 2>&1)
|
||||
|
||||
echo " Response: $RESPONSE"
|
||||
|
||||
if echo "$RESPONSE" | grep -q "choices"; then
|
||||
echo " ✅ Inference endpoint working"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Local Backend is working correctly!"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
else
|
||||
echo " ❌ Inference test failed"
|
||||
echo ""
|
||||
echo "Troubleshooting:"
|
||||
echo " 1. Check Claude CLI: echo 'test' | claude"
|
||||
echo " 2. Check logs: tail -f proxy.log"
|
||||
echo " 3. Restart proxy: ./stop-proxy.sh && ./start-proxy.sh"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,217 +0,0 @@
|
||||
# Agentic Writer Local Backend
|
||||
|
||||
Run unlimited AI content generation on your own machine using your Claude CLI + Z.ai/Anthropic account.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure you have:
|
||||
|
||||
- ✅ **Claude CLI** installed and configured
|
||||
- Get it: [https://claude.ai/code](https://claude.ai/code) or [https://z.ai](https://z.ai)
|
||||
- Verify: `claude --version` or `which claude`
|
||||
- ✅ **Node.js 18+** installed
|
||||
- Download: [https://nodejs.org](https://nodejs.org)
|
||||
- Verify: `node --version`
|
||||
- ✅ **Z.ai Coding Plan** or **Anthropic API key** configured in Claude CLI
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Extract Package
|
||||
|
||||
```bash
|
||||
unzip agentic-writer-local-backend.zip
|
||||
cd agentic-writer-local-backend
|
||||
```
|
||||
|
||||
### 2. Start the Proxy
|
||||
|
||||
```bash
|
||||
chmod +x *.sh
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
You'll see:
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════
|
||||
✅ Local Backend Running!
|
||||
═══════════════════════════════════════════════════
|
||||
|
||||
Your Configuration:
|
||||
Base URL: http://192.168.1.105:8080
|
||||
API Key: dummy
|
||||
Model: claude-local
|
||||
```
|
||||
|
||||
### 3. Configure WordPress Plugin
|
||||
|
||||
1. Open **WP Admin** → **Agentic Writer** → **Settings** → **Local Backend**
|
||||
2. Paste the **Base URL** shown above
|
||||
3. API Key: `dummy`
|
||||
4. Click **Test Connection** → should show ✅
|
||||
5. Start generating content!
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
./start-proxy.sh # Start proxy (runs in background)
|
||||
./stop-proxy.sh # Stop proxy
|
||||
./test-connection.sh # Test if proxy responds
|
||||
./get-local-ip.sh # Find your local IP address
|
||||
tail -f proxy.log # View real-time logs
|
||||
```
|
||||
|
||||
## Firewall Setup
|
||||
|
||||
The proxy needs to accept connections from your WordPress site.
|
||||
|
||||
### macOS
|
||||
|
||||
1. **System Settings** → **Network** → **Firewall**
|
||||
2. Click **Options** → **Add** → Select `node`
|
||||
3. Set to **Allow incoming connections**
|
||||
|
||||
### Linux (ufw)
|
||||
|
||||
```bash
|
||||
sudo ufw allow 8080/tcp
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. **Windows Defender Firewall** → **Advanced Settings**
|
||||
2. **Inbound Rules** → **New Rule**
|
||||
3. **Port** → TCP **8080** → **Allow**
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
WordPress Plugin → HTTP POST → Local Proxy (port 8080)
|
||||
↓
|
||||
Spawns Claude CLI
|
||||
↓
|
||||
Returns AI Response
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 🆓 **Free**: Uses your existing Z.ai/Anthropic subscription
|
||||
- 🔒 **Private**: Content never leaves your network
|
||||
- ⚡ **Fast**: LAN latency (~50-200ms)
|
||||
- 🚀 **Unlimited**: No rate limits, no token counting
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed solutions.
|
||||
|
||||
### Quick Fixes
|
||||
|
||||
**"Connection failed" in plugin:**
|
||||
```bash
|
||||
# Check proxy is running
|
||||
ps aux | grep claude-proxy
|
||||
|
||||
# Restart if needed
|
||||
./stop-proxy.sh && ./start-proxy.sh
|
||||
```
|
||||
|
||||
**"Claude CLI not found":**
|
||||
```bash
|
||||
# Verify Claude is installed
|
||||
which claude
|
||||
claude --version
|
||||
|
||||
# Test Claude works
|
||||
echo "Hello" | claude
|
||||
```
|
||||
|
||||
**"Wrong IP address":**
|
||||
```bash
|
||||
# Find your correct IP
|
||||
./get-local-ip.sh
|
||||
|
||||
# Or manually:
|
||||
# macOS: ipconfig getifaddr en0
|
||||
# Linux: ip route get 1 | awk '{print $7}'
|
||||
```
|
||||
|
||||
**Port 8080 already in use:**
|
||||
```bash
|
||||
# Find what's using it
|
||||
lsof -i :8080
|
||||
|
||||
# Change port (edit claude-proxy.js)
|
||||
PORT=9000 node claude-proxy.js
|
||||
# Update plugin Base URL to: http://your-ip:9000
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Proxy binds to `0.0.0.0` (all network interfaces) for LAN access
|
||||
- No authentication by design (LAN trust model)
|
||||
- All request prompts are logged to `proxy.log`
|
||||
- For internet exposure, use ngrok/reverse proxy with authentication
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Use different port (default: 8080)
|
||||
PORT=9000 ./start-proxy.sh
|
||||
|
||||
# Production mode
|
||||
NODE_ENV=production
|
||||
|
||||
# Brave Search API (for web search capability)
|
||||
export BRAVE_SEARCH_API_KEY="your-brave-api-key"
|
||||
```
|
||||
|
||||
### Enabling Web Search (Brave Search)
|
||||
|
||||
To enable web search in your AI responses:
|
||||
|
||||
1. **Get a Brave Search API key** from [https://brave.com/search/api/](https://brave.com/search/api/)
|
||||
|
||||
2. **Configure it in one of these ways:**
|
||||
|
||||
**Option 1: Add to `.env` file (recommended for this proxy)**
|
||||
```bash
|
||||
echo 'BRAVE_SEARCH_API_KEY="BSA03Yj-your-key-here"' > .env
|
||||
```
|
||||
|
||||
**Option 2: Add to Claude Code settings**
|
||||
Add to `~/.claude/settings.json`:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"BRAVE_SEARCH_API_KEY": "your-key-here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Add to shell profile**
|
||||
```bash
|
||||
export BRAVE_SEARCH_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
3. **Restart the proxy**:
|
||||
```bash
|
||||
./stop-proxy.sh && ./start-proxy.sh
|
||||
```
|
||||
|
||||
When the proxy starts, you should see:
|
||||
```
|
||||
Brave Search:
|
||||
API Key: CONFIGURED
|
||||
```
|
||||
|
||||
**Note:** Web search must also be enabled in the WordPress plugin settings (Agentic Writer → Settings → General → Search → Enable). The plugin will automatically use search results when planning or researching topics.
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [Plugin Docs](https://github.com/your/plugin)
|
||||
- **Issues**: [GitHub Issues](https://github.com/your/plugin/issues)
|
||||
- **Community**: [Discord](https://discord.gg/your-server)
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0+ - Same as WP Agentic Writer plugin
|
||||
@@ -1,339 +0,0 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
Common issues and solutions for Agentic Writer Local Backend.
|
||||
|
||||
## Connection Issues
|
||||
|
||||
### "Connection timeout" in Plugin
|
||||
|
||||
**Symptoms:**
|
||||
- Plugin shows "Connection timeout" error
|
||||
- Test connection fails
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check proxy is running:**
|
||||
```bash
|
||||
ps aux | grep claude-proxy
|
||||
```
|
||||
|
||||
2. **Restart proxy:**
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
3. **Check logs:**
|
||||
```bash
|
||||
tail -f proxy.log
|
||||
```
|
||||
|
||||
4. **Verify IP address:**
|
||||
```bash
|
||||
./get-local-ip.sh
|
||||
```
|
||||
|
||||
### "Connection refused"
|
||||
|
||||
**Cause:** Proxy not running or wrong IP
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Start proxy:**
|
||||
```bash
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
2. **Check firewall:**
|
||||
- macOS: System Settings → Network → Firewall → Allow Node.js
|
||||
- Linux: `sudo ufw allow 8080/tcp`
|
||||
- Windows: Defender Firewall → Allow port 8080
|
||||
|
||||
3. **Test locally first:**
|
||||
```bash
|
||||
curl http://localhost:8080/ping
|
||||
# Should return: pong
|
||||
```
|
||||
|
||||
## Claude CLI Issues
|
||||
|
||||
### "Claude CLI not found"
|
||||
|
||||
**Verify installation:**
|
||||
```bash
|
||||
which claude
|
||||
# macOS: /opt/homebrew/bin/claude or /usr/local/bin/claude
|
||||
# Linux: ~/.local/bin/claude or /usr/bin/claude
|
||||
```
|
||||
|
||||
**Fix PATH:**
|
||||
```bash
|
||||
# Add to ~/.zshrc or ~/.bashrc
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**Reinstall Claude CLI:**
|
||||
- Visit: [https://claude.ai/code](https://claude.ai/code)
|
||||
- Follow installation instructions
|
||||
|
||||
### "No response from Claude"
|
||||
|
||||
**Test Claude manually:**
|
||||
```bash
|
||||
echo "Hello, reply with: Test successful" | claude
|
||||
```
|
||||
|
||||
**Check authentication:**
|
||||
```bash
|
||||
claude --version
|
||||
# Should show version and auth status
|
||||
```
|
||||
|
||||
**Reconfigure Claude:**
|
||||
- Check Z.ai account: [https://z.ai](https://z.ai)
|
||||
- Or Anthropic API key setup
|
||||
|
||||
## Network Issues
|
||||
|
||||
### Wrong IP Address Detected
|
||||
|
||||
**Find correct IP:**
|
||||
```bash
|
||||
# macOS
|
||||
ipconfig getifaddr en0 # WiFi
|
||||
ipconfig getifaddr en1 # Ethernet
|
||||
|
||||
# Linux
|
||||
ip route get 1 | awk '{print $7}'
|
||||
hostname -I
|
||||
|
||||
# Windows
|
||||
ipconfig
|
||||
# Look for "IPv4 Address" under active adapter
|
||||
```
|
||||
|
||||
**Update plugin settings:**
|
||||
- Use the correct IP in Base URL: `http://CORRECT-IP:8080`
|
||||
|
||||
### Port 8080 Already in Use
|
||||
|
||||
**Find what's using it:**
|
||||
```bash
|
||||
lsof -i :8080
|
||||
# or
|
||||
netstat -anp | grep 8080
|
||||
```
|
||||
|
||||
**Change port:**
|
||||
|
||||
1. Edit `claude-proxy.js`:
|
||||
```javascript
|
||||
const PORT = process.env.PORT || 9000; // Change 8080 to 9000
|
||||
```
|
||||
|
||||
2. Restart proxy:
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
PORT=9000 ./start-proxy.sh
|
||||
```
|
||||
|
||||
3. Update plugin Base URL: `http://your-ip:9000`
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Response Times
|
||||
|
||||
**Normal latency:**
|
||||
- Local network: 50-200ms
|
||||
- Claude CLI processing: 2-30 seconds depending on prompt
|
||||
|
||||
**If consistently slow:**
|
||||
|
||||
1. **Check network:**
|
||||
```bash
|
||||
ping 192.168.1.105 # Your proxy IP
|
||||
```
|
||||
|
||||
2. **Monitor logs:**
|
||||
```bash
|
||||
tail -f proxy.log
|
||||
```
|
||||
|
||||
3. **Check machine resources:**
|
||||
- CPU usage: Claude CLI is CPU-intensive
|
||||
- Memory: Ensure sufficient RAM available
|
||||
|
||||
### Proxy Crashes
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
cat proxy.log | tail -50
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- Out of memory: Close other applications
|
||||
- Claude CLI timeout: Increase timeout in `claude-proxy.js`
|
||||
- Malformed requests: Check plugin version compatibility
|
||||
|
||||
**Restart with clean state:**
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
rm proxy.log
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
## Plugin Integration Issues
|
||||
|
||||
### "Invalid response format"
|
||||
|
||||
**Cause:** Claude response doesn't match expected JSON format
|
||||
|
||||
**Debug:**
|
||||
1. Check `proxy.log` for actual Claude output
|
||||
2. Test manually:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"messages":[{"role":"user","content":"Hello"}]}'
|
||||
```
|
||||
|
||||
3. Update Claude CLI if outdated:
|
||||
```bash
|
||||
claude --version
|
||||
# Upgrade if needed
|
||||
```
|
||||
|
||||
### Cost Tracking Shows $0
|
||||
|
||||
**Expected behavior:** Local backend is free, plugin should show `$0.00 (Local)`
|
||||
|
||||
**If concerned:**
|
||||
- This is correct - local backend has no API costs
|
||||
- Dashboard should show "X requests local (free)"
|
||||
|
||||
## Advanced Troubleshooting
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Edit `claude-proxy.js`:
|
||||
```javascript
|
||||
const DEBUG = true; // Add at top of file
|
||||
|
||||
// In /v1/messages handler:
|
||||
if (DEBUG) {
|
||||
console.log('Full request:', JSON.stringify(req.body, null, 2));
|
||||
console.log('Full response:', output);
|
||||
}
|
||||
```
|
||||
|
||||
### Test with curl
|
||||
|
||||
**Ping:**
|
||||
```bash
|
||||
curl http://localhost:8080/ping
|
||||
# Expected: pong
|
||||
```
|
||||
|
||||
**Inference:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role": "user", "content": "Reply with: Test successful"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected response:**
|
||||
```json
|
||||
{
|
||||
"id": "local-1234567890",
|
||||
"object": "chat.completion",
|
||||
"model": "claude-local",
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "Test successful"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Permissions Issues (macOS)
|
||||
|
||||
**Make scripts executable:**
|
||||
```bash
|
||||
chmod +x start-proxy.sh stop-proxy.sh test-connection.sh get-local-ip.sh
|
||||
```
|
||||
|
||||
**If "permission denied":**
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la *.sh
|
||||
|
||||
# Reset if needed
|
||||
chmod 755 *.sh
|
||||
```
|
||||
|
||||
## Still Having Issues?
|
||||
|
||||
1. **Check system requirements:**
|
||||
- Node.js 18+: `node --version`
|
||||
- Claude CLI installed: `which claude`
|
||||
- Sufficient disk space: `df -h`
|
||||
|
||||
2. **Collect diagnostic info:**
|
||||
```bash
|
||||
echo "Node version:" $(node --version)
|
||||
echo "Claude path:" $(which claude)
|
||||
echo "Local IP:" $(./get-local-ip.sh)
|
||||
echo "Proxy status:" $(ps aux | grep claude-proxy)
|
||||
tail -20 proxy.log
|
||||
```
|
||||
|
||||
3. **Reset everything:**
|
||||
```bash
|
||||
./stop-proxy.sh
|
||||
rm -rf node_modules proxy.log proxy.pid
|
||||
npm install
|
||||
./start-proxy.sh
|
||||
```
|
||||
|
||||
4. **Get help:**
|
||||
- GitHub Issues: [Report Bug](https://github.com/your/plugin/issues)
|
||||
- Discord Community: [Join Chat](https://discord.gg/your-server)
|
||||
- Include: OS, Node version, Claude CLI version, error logs
|
||||
|
||||
## Environment-Specific Notes
|
||||
|
||||
### macOS
|
||||
|
||||
- Default Claude path: `/opt/homebrew/bin/claude`
|
||||
- Firewall: System Settings → Network → Firewall
|
||||
- IP detection: `ipconfig getifaddr en0`
|
||||
|
||||
### Linux
|
||||
|
||||
- Default Claude path: `~/.local/bin/claude`
|
||||
- Firewall: `sudo ufw allow 8080/tcp`
|
||||
- IP detection: `ip route get 1 | awk '{print $7}'`
|
||||
|
||||
### Windows
|
||||
|
||||
- Claude path varies, check `where claude`
|
||||
- Firewall: Windows Defender → Allow port 8080
|
||||
- IP detection: `ipconfig` (look for IPv4)
|
||||
- Scripts: Use Git Bash or WSL to run `.sh` scripts
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **LAN only:** Don't expose proxy to internet without authentication
|
||||
2. **Firewall:** Restrict to specific IPs if on shared network
|
||||
3. **Logs:** `proxy.log` contains all prompts - review periodically
|
||||
4. **Updates:** Keep Node.js and Claude CLI updated
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-02-27
|
||||
**Version:** 1.0.0
|
||||
Binary file not shown.
@@ -1,279 +0,0 @@
|
||||
const express = require('express');
|
||||
const { spawn } = require('child_process');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Try multiple sources for Brave API Key (in order of priority):
|
||||
// 1. Environment variable
|
||||
// 2. .env file in proxy directory
|
||||
// 3. ~/.claude/settings.json (Claude Code config)
|
||||
function getBraveApiKey() {
|
||||
// 1. Check environment variable first
|
||||
if (process.env.BRAVE_SEARCH_API_KEY) {
|
||||
return process.env.BRAVE_SEARCH_API_KEY;
|
||||
}
|
||||
|
||||
// 2. Check .env file in proxy directory
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||
const match = envContent.match(/BRAVE_SEARCH_API_KEY\s*=\s*(.+)/m);
|
||||
if (match) {
|
||||
return match[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check Claude Code settings.json
|
||||
const claudeSettingsPath = path.join(process.env.HOME || '/root', '.claude', 'settings.json');
|
||||
if (fs.existsSync(claudeSettingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
|
||||
if (settings.env?.BRAVE_SEARCH_API_KEY) {
|
||||
return settings.env.BRAVE_SEARCH_API_KEY;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/ping', (req, res) => {
|
||||
const status = {
|
||||
status: 'pong',
|
||||
braveSearchConfigured: !!BRAVE_API_KEY,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
res.json(status);
|
||||
});
|
||||
|
||||
// Main inference endpoint (OpenAI-compatible format)
|
||||
app.post('/v1/messages', async (req, res) => {
|
||||
const { messages, stream } = req.body;
|
||||
|
||||
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
message: 'Invalid request: messages array required'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if web search is requested (via X-Search-Enabled header)
|
||||
const webSearchEnabled = req.headers['x-search-enabled'] === 'true';
|
||||
const searchQuery = req.headers['x-search-query'] || '';
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Request from:', req.ip);
|
||||
console.log('Web Search:', webSearchEnabled ? 'ENABLED' : 'disabled');
|
||||
if (searchQuery) {
|
||||
console.log('Search Query:', searchQuery.substring(0, 100) + '...');
|
||||
}
|
||||
const braveApiKey = getBraveApiKey();
|
||||
console.log('Brave API Key:', braveApiKey ? 'CONFIGURED' : 'NOT SET');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
// If web search is enabled and we have a query, fetch search results first
|
||||
let searchContext = '';
|
||||
if (webSearchEnabled && searchQuery && braveApiKey) {
|
||||
console.log('Fetching web search results...');
|
||||
try {
|
||||
searchContext = await fetchBraveSearchResults(searchQuery, braveApiKey);
|
||||
console.log('Search results fetched:', searchContext.length, 'chars');
|
||||
} catch (err) {
|
||||
console.error('Search error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Build conversation context from messages array
|
||||
// Include previous messages for context continuity
|
||||
let conversationPrompt = '';
|
||||
for (const msg of messages) {
|
||||
const role = msg.role === 'assistant' ? 'Assistant' : 'User';
|
||||
conversationPrompt += `${role}: ${msg.content}\n\n`;
|
||||
}
|
||||
|
||||
let prompt = conversationPrompt.trim();
|
||||
|
||||
// Prepend search context if available
|
||||
if (searchContext) {
|
||||
prompt = `WEB SEARCH RESULTS:\n${searchContext}\n\n---\n\nUSER QUERY:\n${prompt}\n\nPlease answer based on the search results above when relevant.`;
|
||||
}
|
||||
|
||||
console.log('Prompt length:', prompt.length, 'chars');
|
||||
console.log('Prompt preview:', prompt.substring(0, 150) + '...');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
// Spawn Claude CLI process
|
||||
const claude = spawn('claude', [], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
claude.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
process.stdout.write('.');
|
||||
});
|
||||
|
||||
claude.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
console.error('Claude stderr:', data.toString());
|
||||
});
|
||||
|
||||
claude.on('close', (code) => {
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Claude exit code:', code);
|
||||
console.log('Response length:', output.length, 'chars');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (code !== 0 || !output.trim()) {
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Claude CLI error',
|
||||
details: errorOutput || 'No response from Claude'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return OpenAI-compatible response format
|
||||
res.json({
|
||||
id: 'local-' + Date.now(),
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: 'claude-local',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: output.trim()
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
claude.on('error', (err) => {
|
||||
console.error('Failed to spawn Claude CLI:', err);
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to spawn Claude CLI',
|
||||
details: err.message
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Send prompt to Claude after brief pause
|
||||
setTimeout(() => {
|
||||
claude.stdin.write(prompt + '\n');
|
||||
claude.stdin.end();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch search results from Brave Search API
|
||||
*/
|
||||
async function fetchBraveSearchResults(query, apiKey, count = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}&count=${count}`;
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Subscription-Token': apiKey
|
||||
}
|
||||
};
|
||||
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
const request = protocol.get(url, options, (response) => {
|
||||
let data = '';
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
if (response.statusCode !== 200) {
|
||||
return reject(new Error(`Brave API error: ${response.statusCode}`));
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const results = json.web?.results || [];
|
||||
|
||||
if (results.length === 0) {
|
||||
return resolve('No search results found.');
|
||||
}
|
||||
|
||||
// Format results for LLM consumption
|
||||
let formatted = 'Search Results:\n\n';
|
||||
|
||||
results.forEach((result, i) => {
|
||||
formatted += `${i + 1}. **${result.title}**\n`;
|
||||
formatted += ` URL: ${result.url}\n`;
|
||||
if (result.description) {
|
||||
formatted += ` Summary: ${result.description}\n`;
|
||||
}
|
||||
formatted += '\n';
|
||||
});
|
||||
|
||||
resolve(formatted);
|
||||
} catch (err) {
|
||||
reject(new Error('Failed to parse Brave response'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
request.setTimeout(10000, () => {
|
||||
request.destroy();
|
||||
reject(new Error('Brave search timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
const braveApiKey = getBraveApiKey();
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
console.log('🚀 Agentic Writer Local Backend v1.1.0');
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
console.log(`Local: http://localhost:${PORT}`);
|
||||
console.log(`Network: http://YOUR-IP:${PORT}`);
|
||||
console.log('');
|
||||
console.log('Plugin Configuration:');
|
||||
console.log(` Base URL: http://YOUR-IP:${PORT}`);
|
||||
console.log(` API Key: dummy`);
|
||||
console.log(` Model: claude-local`);
|
||||
console.log('');
|
||||
console.log('Brave Search:');
|
||||
console.log(` API Key: ${braveApiKey ? 'CONFIGURED' : 'NOT SET'}`);
|
||||
console.log('');
|
||||
console.log('Web search works when Brave API key is found from:');
|
||||
console.log(' 1. Environment: export BRAVE_SEARCH_API_KEY="key"');
|
||||
console.log(' 2. .env file: BRAVE_SEARCH_API_KEY=key');
|
||||
console.log(' 3. ~/.claude/settings.json env.BRAVE_SEARCH_API_KEY');
|
||||
console.log('');
|
||||
console.log('Restart proxy after adding key: ./stop-proxy.sh && ./start-proxy.sh');
|
||||
console.log('');
|
||||
console.log('Health check: GET /ping');
|
||||
console.log('Inference: POST /v1/messages');
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Detecting your local IP address..."
|
||||
echo ""
|
||||
|
||||
# Detect local IP based on OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS - try en0 (WiFi) then en1 (Ethernet)
|
||||
IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "")
|
||||
INTERFACE=$(ifconfig en0 &>/dev/null && echo "en0 (WiFi)" || echo "en1 (Ethernet)")
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || hostname -I | awk '{print $1}')
|
||||
INTERFACE="default"
|
||||
else
|
||||
# Windows or unknown
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||||
INTERFACE="unknown"
|
||||
fi
|
||||
|
||||
if [ -z "$IP" ]; then
|
||||
echo "❌ Could not detect IP address automatically"
|
||||
echo ""
|
||||
echo "Manual detection:"
|
||||
echo " macOS: ipconfig getifaddr en0"
|
||||
echo " Linux: ip route get 1 | awk '{print \$7}'"
|
||||
echo " Windows: ipconfig (look for IPv4 Address)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Your local IP: $IP ($INTERFACE)"
|
||||
echo ""
|
||||
echo "Use this in your plugin settings:"
|
||||
echo " Base URL: http://$IP:8080"
|
||||
828
downloads/package-lock.json
generated
828
downloads/package-lock.json
generated
@@ -1,828 +0,0 @@
|
||||
{
|
||||
"name": "agentic-writer-local-backend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agentic-writer-local-backend",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-2.0+",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "agentic-writer-local-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Local backend proxy for WP Agentic Writer using Claude CLI",
|
||||
"main": "claude-proxy.js",
|
||||
"scripts": {
|
||||
"start": "node claude-proxy.js",
|
||||
"test": "curl http://localhost:8080/ping"
|
||||
},
|
||||
"keywords": [
|
||||
"wordpress",
|
||||
"ai",
|
||||
"claude",
|
||||
"proxy"
|
||||
],
|
||||
"author": "WP Agentic Writer",
|
||||
"license": "GPL-2.0+",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Starting Agentic Writer Local Backend..."
|
||||
echo ""
|
||||
|
||||
# Check dependencies
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ Node.js not found. Install from https://nodejs.org/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v claude &> /dev/null; then
|
||||
echo "❌ Claude CLI not found. Install and configure first."
|
||||
echo " Check: https://claude.ai/code or https://z.ai"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Auto-install express if needed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Load environment variables from .env file if it exists
|
||||
if [ -f .env ]; then
|
||||
echo "📋 Loading environment from .env file..."
|
||||
set -a # automatically export all variables created
|
||||
source .env
|
||||
set +a # stop auto-export
|
||||
fi
|
||||
|
||||
# Detect local IP
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "127.0.0.1")
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
LOCAL_IP=$(ip route get 1 | awk '{print $7;exit}' 2>/dev/null || echo "127.0.0.1")
|
||||
else
|
||||
# Windows/other
|
||||
LOCAL_IP="127.0.0.1"
|
||||
fi
|
||||
|
||||
echo "✅ Dependencies OK"
|
||||
echo "✅ Claude CLI found: $(which claude)"
|
||||
echo ""
|
||||
echo "Starting proxy server..."
|
||||
echo ""
|
||||
|
||||
# Start server in background
|
||||
nohup node claude-proxy.js > proxy.log 2>&1 &
|
||||
PID=$!
|
||||
echo $PID > proxy.pid
|
||||
|
||||
# Wait for server to boot
|
||||
sleep 2
|
||||
|
||||
# Test if running
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
echo "✅ Local Backend Running!"
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Your Configuration:"
|
||||
echo " Base URL: http://$LOCAL_IP:8080"
|
||||
echo " API Key: dummy"
|
||||
echo " Model: claude-local"
|
||||
echo ""
|
||||
if [ -n "$BRAVE_SEARCH_API_KEY" ]; then
|
||||
echo "Brave Search: ✅ CONFIGURED"
|
||||
else
|
||||
echo "Brave Search: ⚠️ NOT SET (web search disabled)"
|
||||
echo " To enable: Add BRAVE_SEARCH_API_KEY to .env file"
|
||||
fi
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo " 1. Open your WordPress Admin"
|
||||
echo " 2. Go to Agentic Writer → Settings → Local Backend"
|
||||
echo " 3. Paste the Base URL above"
|
||||
echo " 4. Click 'Test Connection'"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Logs: tail -f proxy.log"
|
||||
echo " Stop: ./stop-proxy.sh"
|
||||
echo " Test: ./test-connection.sh"
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
else
|
||||
echo "❌ Failed to start. Check proxy.log for errors."
|
||||
cat proxy.log
|
||||
rm -f proxy.pid
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f proxy.pid ]; then
|
||||
PID=$(cat proxy.pid)
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
kill $PID
|
||||
rm proxy.pid
|
||||
echo "🛑 Local Backend stopped (PID: $PID)"
|
||||
else
|
||||
echo "⚠️ No process found with PID: $PID"
|
||||
rm proxy.pid
|
||||
fi
|
||||
else
|
||||
# Fallback: kill by process name
|
||||
pkill -f claude-proxy.js
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "🛑 Stopped all claude-proxy processes"
|
||||
else
|
||||
echo "ℹ️ No claude-proxy processes running"
|
||||
fi
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Testing local backend connection..."
|
||||
echo ""
|
||||
|
||||
# Test /ping endpoint
|
||||
echo "1. Testing health check..."
|
||||
PING_RESPONSE=$(curl -s http://localhost:8080/ping 2>&1)
|
||||
|
||||
if [ "$PING_RESPONSE" = "pong" ]; then
|
||||
echo " ✅ Health check passed"
|
||||
else
|
||||
echo " ❌ Health check failed"
|
||||
echo " Response: $PING_RESPONSE"
|
||||
echo ""
|
||||
echo "Is the proxy running? Check: ps aux | grep claude-proxy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test /v1/messages endpoint
|
||||
echo "2. Testing inference..."
|
||||
RESPONSE=$(curl -s -X POST http://localhost:8080/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"messages":[{"role":"user","content":"Reply with exactly: Test successful"}]}' 2>&1)
|
||||
|
||||
echo " Response: $RESPONSE"
|
||||
|
||||
if echo "$RESPONSE" | grep -q "choices"; then
|
||||
echo " ✅ Inference endpoint working"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Local Backend is working correctly!"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
else
|
||||
echo " ❌ Inference test failed"
|
||||
echo ""
|
||||
echo "Troubleshooting:"
|
||||
echo " 1. Check Claude CLI: echo 'test' | claude"
|
||||
echo " 2. Check logs: tail -f proxy.log"
|
||||
echo " 3. Restart proxy: ./stop-proxy.sh && ./start-proxy.sh"
|
||||
exit 1
|
||||
fi
|
||||
530
includes/class-context-builder.php
Normal file
530
includes/class-context-builder.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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',
|
||||
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()
|
||||
// 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,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$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' )
|
||||
);
|
||||
if ( is_wp_error( $health_response ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$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(
|
||||
|
||||
@@ -514,6 +514,49 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
|
||||
return __( 'Please go to Settings → Models and select a different model that is available on OpenRouter.', 'wp-agentic-writer' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build optional request-level OpenRouter provider routing preferences.
|
||||
*
|
||||
* This is intentionally settings-driven. BYOK users may pin a provider and
|
||||
* disable fallbacks, but the plugin should not assume every OpenRouter model
|
||||
* should use OpenAI, Anthropic, Azure, or any other provider.
|
||||
*
|
||||
* @since 0.2.3
|
||||
* @param array $options Request options.
|
||||
* @return array Provider routing preferences.
|
||||
*/
|
||||
private function get_provider_routing_preferences( $options = array() ) {
|
||||
if ( isset( $options['provider'] ) && is_array( $options['provider'] ) ) {
|
||||
return $options['provider'];
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'openrouter_provider_routing', $options ) && false === (bool) $options['openrouter_provider_routing'] ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$settings = get_option( 'wp_agentic_writer_settings', array() );
|
||||
$enabled = ! empty( $settings['openrouter_provider_routing_enabled'] );
|
||||
$provider_slug = isset( $settings['openrouter_provider_slug'] ) ? sanitize_key( $settings['openrouter_provider_slug'] ) : '';
|
||||
|
||||
if ( ! $enabled || '' === $provider_slug || 'auto' === $provider_slug ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$routing = array(
|
||||
'order' => array( $provider_slug ),
|
||||
);
|
||||
|
||||
if ( ! empty( $settings['openrouter_provider_only'] ) ) {
|
||||
$routing['only'] = array( $provider_slug );
|
||||
}
|
||||
|
||||
if ( isset( $settings['openrouter_allow_provider_fallbacks'] ) ) {
|
||||
$routing['allow_fallbacks'] = (bool) $settings['openrouter_allow_provider_fallbacks'];
|
||||
}
|
||||
|
||||
return $routing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
@@ -605,6 +648,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
|
||||
'include' => true,
|
||||
),
|
||||
);
|
||||
$provider_routing = $this->get_provider_routing_preferences( $options );
|
||||
if ( ! empty( $provider_routing ) ) {
|
||||
$body['provider'] = $provider_routing;
|
||||
}
|
||||
|
||||
// Add optional parameters.
|
||||
if ( isset( $options['max_tokens'] ) ) {
|
||||
@@ -737,7 +784,21 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
|
||||
// Validate model availability before making API call
|
||||
$model_validation = $this->validate_model_availability( $model );
|
||||
if ( is_wp_error( $model_validation ) ) {
|
||||
return $model_validation;
|
||||
// Auto-fallback: try registry fallback model instead of hard-failing
|
||||
$fallback_model = WPAW_Model_Registry::get_fallback_model( $type );
|
||||
if ( $fallback_model && $fallback_model !== $model ) {
|
||||
$fallback_validation = $this->validate_model_availability( $fallback_model );
|
||||
if ( true === $fallback_validation ) {
|
||||
$model = $fallback_model;
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( "WPAW: Model unavailable, auto-fallback to: {$fallback_model}" );
|
||||
}
|
||||
} else {
|
||||
return $model_validation;
|
||||
}
|
||||
} else {
|
||||
return $model_validation;
|
||||
}
|
||||
}
|
||||
|
||||
// Build request body.
|
||||
@@ -752,6 +813,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
|
||||
'include' => true,
|
||||
),
|
||||
);
|
||||
$provider_routing = $this->get_provider_routing_preferences( $options );
|
||||
if ( ! empty( $provider_routing ) ) {
|
||||
$body['provider'] = $provider_routing;
|
||||
}
|
||||
|
||||
// Add optional parameters.
|
||||
if ( isset( $options['max_tokens'] ) ) {
|
||||
|
||||
@@ -43,6 +43,7 @@ class WP_Agentic_Writer_Provider_Manager {
|
||||
public static function get_provider_for_task( $type ) {
|
||||
$settings = get_option( 'wp_agentic_writer_settings', array() );
|
||||
$task_providers = $settings['task_providers'] ?? array();
|
||||
$allow_openrouter_fallback = ! empty( $settings['allow_openrouter_fallback'] );
|
||||
|
||||
// Determine which provider to use for this task
|
||||
$requested_provider = $task_providers[ $type ] ?? 'openrouter';
|
||||
@@ -58,11 +59,26 @@ class WP_Agentic_Writer_Provider_Manager {
|
||||
// Get provider instance with fallback logic
|
||||
$provider = self::get_provider_instance( $requested_provider, $type );
|
||||
|
||||
// If provider not configured or unavailable, fallback to OpenRouter
|
||||
$can_fallback_to_openrouter = ( 'openrouter' === $requested_provider ) || $allow_openrouter_fallback;
|
||||
|
||||
// If provider not configured or unavailable.
|
||||
if ( ! $provider || ! $provider->is_configured() ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( "Provider '{$requested_provider}' not available for task '{$type}', using OpenRouter fallback" );
|
||||
error_log( "Provider '{$requested_provider}' not available for task '{$type}'" );
|
||||
}
|
||||
|
||||
// Never silently spend OpenRouter credits when user selected another provider.
|
||||
if ( ! $can_fallback_to_openrouter ) {
|
||||
$warnings[] = "Provider '{$requested_provider}' unavailable. No automatic fallback was applied.";
|
||||
return new WPAW_Provider_Selection_Result(
|
||||
$provider,
|
||||
$requested_provider,
|
||||
$requested_provider,
|
||||
false,
|
||||
$warnings
|
||||
);
|
||||
}
|
||||
|
||||
$warnings[] = "Provider '{$requested_provider}' unavailable, fell back to OpenRouter";
|
||||
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
||||
$actual_provider = 'openrouter';
|
||||
@@ -74,12 +90,16 @@ class WP_Agentic_Writer_Provider_Manager {
|
||||
$test_result = $provider->test_connection();
|
||||
if ( is_wp_error( $test_result ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( "Local Backend not reachable for task '{$type}', using OpenRouter fallback. Error: " . $test_result->get_error_message() );
|
||||
error_log( "Local Backend not reachable for task '{$type}'. Error: " . $test_result->get_error_message() );
|
||||
}
|
||||
if ( $can_fallback_to_openrouter ) {
|
||||
$warnings[] = "Local Backend not reachable, fell back to OpenRouter.";
|
||||
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
||||
$actual_provider = 'openrouter';
|
||||
$fallback_used = true;
|
||||
} else {
|
||||
$warnings[] = "Local Backend not reachable. No automatic fallback was applied.";
|
||||
}
|
||||
$warnings[] = "Local Backend not reachable, fell back to OpenRouter";
|
||||
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
||||
$actual_provider = 'openrouter';
|
||||
$fallback_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
55
scripts/build-local-backend-zip.sh
Executable file
55
scripts/build-local-backend-zip.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build a clean distributable ZIP for Local Backend package.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-local-backend-zip.sh
|
||||
# ./scripts/build-local-backend-zip.sh /path/to/source /path/to/output.zip
|
||||
|
||||
SOURCE_DIR="${1:-/Users/dwindown/Documents/agentic-writer-local-backend}"
|
||||
OUTPUT_ZIP="${2:-/private/tmp/agentic-writer-local-backend.zip}"
|
||||
|
||||
if [[ ! -d "$SOURCE_DIR" ]]; then
|
||||
echo "Source directory not found: $SOURCE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v zip >/dev/null 2>&1; then
|
||||
echo "'zip' command is required but not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
PKG_DIR="$TMP_DIR/agentic-writer-local-backend"
|
||||
mkdir -p "$PKG_DIR"
|
||||
|
||||
# Copy only distributable files/directories (exclude runtime/build/noise files).
|
||||
rsync -a \
|
||||
--exclude '.git/' \
|
||||
--exclude '.github/' \
|
||||
--exclude '.claude/' \
|
||||
--exclude '.sixth/' \
|
||||
--exclude 'node_modules/' \
|
||||
--exclude '.env' \
|
||||
--exclude '.env.*' \
|
||||
--exclude '*.log' \
|
||||
--exclude 'logs/' \
|
||||
--exclude '.DS_Store' \
|
||||
--exclude '__MACOSX/' \
|
||||
--exclude '*.zip' \
|
||||
"$SOURCE_DIR/" "$PKG_DIR/"
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_ZIP")"
|
||||
rm -f "$OUTPUT_ZIP"
|
||||
|
||||
(
|
||||
cd "$TMP_DIR"
|
||||
zip -r "$OUTPUT_ZIP" "agentic-writer-local-backend" >/dev/null
|
||||
)
|
||||
|
||||
echo "Built package:"
|
||||
echo " Source: $SOURCE_DIR"
|
||||
echo " Output: $OUTPUT_ZIP"
|
||||
@@ -48,10 +48,33 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="p-3 rounded bg-warning bg-opacity-10 text-center">
|
||||
<div class="fs-4 fw-bold text-warning" id="wpaw-stat-avg">$0.0000</div>
|
||||
<div class="text-muted small"><?php esc_html_e( 'Avg Per Post', 'wp-agentic-writer' ); ?></div>
|
||||
<div class="text-muted small d-flex justify-content-center align-items-center gap-1">
|
||||
<span><?php esc_html_e( 'Avg Per Post', 'wp-agentic-writer' ); ?></span>
|
||||
<span class="dashicons dashicons-info-outline" title="<?php esc_attr_e( 'All-time OpenRouter cost divided by total posts with OpenRouter usage.', 'wp-agentic-writer' ); ?>"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0" id="wpaw-action-summary-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Action', 'wp-agentic-writer' ); ?></th>
|
||||
<th class="text-end"><?php esc_html_e( 'Calls', 'wp-agentic-writer' ); ?></th>
|
||||
<th class="text-end"><?php esc_html_e( 'Total Cost', 'wp-agentic-writer' ); ?></th>
|
||||
<th class="text-end"><?php esc_html_e( 'Avg / Call', 'wp-agentic-writer' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wpaw-action-summary-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-3"><?php esc_html_e( 'Loading action summary...', 'wp-agentic-writer' ); ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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' );
|
||||
|
||||
|
||||
Reference in New Issue
Block a user