From 8b43d2e0a893a51f37a1f592d14800849c6b039a Mon Sep 17 00:00:00 2001 From: Deep7285 Date: Tue, 28 Apr 2026 20:28:31 +0530 Subject: [PATCH 01/11] fix: resolve login BOM error, PBKDF2 limit, and cross-site session auth - worker.ts: add stripBom() helper; replace all env.USERS.get(...,"json") calls with getKVJson() to handle UTF-8 BOM in KV-stored values - worker.ts: add parseJsonBody() to strip BOM from incoming request bodies - worker.ts: add getBearerToken() helper; login now returns token in response body; guardExtract and handleLogout accept Authorization: Bearer header as fallback when cross-site cookies are blocked by the browser - worker.ts: fix malformed Set-Cookie header (two cookies were comma-concatenated into one header; now sent as separate Set-Cookie headers) - extractor-client.js: store session token in sessionStorage after login; send Authorization: Bearer header on extract and logout requests - add-user.ps1: fix UTF-8 BOM written to temp file by replacing [System.Text.Encoding]::UTF8 with New-Object System.Text.UTF8Encoding $false - make-user.mjs / write-user.mjs: lower PBKDF2 iterations from 120000 to 100000 to stay within Cloudflare Workers crypto.subtle limit Co-Authored-By: Claude Sonnet 4.6 --- add-user.ps1 | 3 +- frontend/extractor-client.js | 13 ++++- src/worker.ts | 101 +++++++++++++++++++++++++++-------- tools/make-user.mjs | 2 +- tools/write-user.mjs | 2 +- 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/add-user.ps1 b/add-user.ps1 index 3fd8913..73d7cc6 100644 --- a/add-user.ps1 +++ b/add-user.ps1 @@ -31,7 +31,8 @@ Write-Host "JSON generated OK" -ForegroundColor Green # Step 2: Write JSON to a temp file (avoids all PowerShell quoting issues) $tmpFile = [System.IO.Path]::GetTempFileName() -[System.IO.File]::WriteAllText($tmpFile, $jsonLine, [System.Text.Encoding]::UTF8) +$utf8NoBom = New-Object System.Text.UTF8Encoding $false +[System.IO.File]::WriteAllText($tmpFile, $jsonLine, $utf8NoBom) Write-Host "Temp file: $tmpFile" diff --git a/frontend/extractor-client.js b/frontend/extractor-client.js index f7ad69a..b034689 100644 --- a/frontend/extractor-client.js +++ b/frontend/extractor-client.js @@ -28,6 +28,9 @@ let isRunning = false; // prevent double-submit function getSessionUser() { return sessionStorage.getItem("dx_user"); } function setSessionUser(u) { sessionStorage.setItem("dx_user", u); } function clearSessionUser() { sessionStorage.removeItem("dx_user"); } +function getSessionToken() { return sessionStorage.getItem("dx_token"); } +function setSessionToken(t) { sessionStorage.setItem("dx_token", t); } +function clearSessionToken() { sessionStorage.removeItem("dx_token"); } // ── Toast notifications ─────────────────────────────────────────────────────── function showToast(message, type = "info", duration = 4000) { @@ -94,11 +97,14 @@ async function apiLogin(username, password, honeypot) { // ── API: Logout ─────────────────────────────────────────────────────────────── async function apiLogout() { + const token = getSessionToken(); await fetch(`${WORKER_ENDPOINT}/api/logout`, { method: "POST", - credentials: "include" + credentials: "include", + headers: token ? { "Authorization": `Bearer ${token}` } : {} }).catch(() => {}); clearSessionUser(); + clearSessionToken(); syncAuthUI(); showToast("Logged out successfully", "info"); } @@ -153,6 +159,7 @@ async function handleLoginSubmit() { try { const data = await apiLogin(username, password, honeypot); setSessionUser(data.username); + if (data.token) setSessionToken(data.token); syncAuthUI(); closeLoginModal(); showToast(`Welcome back, ${data.username}!`, "success"); @@ -240,9 +247,11 @@ async function postToWorker({ imagesDataUrls = [], docText = "" }) { for (const d of imagesDataUrls) form.append("images_dataurl[]", d); if (docText?.trim()) form.append("doc_text", docText.trim()); + const token = getSessionToken(); const resp = await fetch(`${WORKER_ENDPOINT}/api/extract`, { method: "POST", - credentials: "include", // ← CRITICAL: session cookie must travel with request + credentials: "include", + headers: token ? { "Authorization": `Bearer ${token}` } : {}, body: form }); diff --git a/src/worker.ts b/src/worker.ts index a791020..17b31b9 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,10 +1,12 @@ // src/worker.ts — Invoice Extractor Worker (Gemini Flash edition) // ------------------------------------------------------------------- // Endpoints: -// POST /api/login { username, password } -// POST /api/logout clears session cookie + KV entry -// POST /api/extract FormData(images_dataurl[], doc_text?) -// Session users: unlimited. Free trial: 3 attempts, 1 page max. +// POST /api/login { username, password } +// POST /api/logout clears session cookie + KV entry +// POST /api/extract FormData(images_dataurl[], doc_text?) +// Session users: unlimited. Free trial: 3 attempts, 1 page max. +// POST /api/sync-chat-history { conversations, messages, etc. } +// Syncs web chat history to KV storage for terminal access // // Wrangler bindings needed: // [vars] ALLOWED_ORIGIN = "https://deep7285.github.io" @@ -77,6 +79,21 @@ const GEMINI_RESPONSE_SCHEMA = { // ------------------------- const JSON_HEADER = { "content-type": "application/json; charset=utf-8" }; +function stripBom(s: string): string { + return s.charCodeAt(0) === 0xFEFF ? s.slice(1) : s; +} + +async function parseJsonBody(req: Request): Promise { + const text = await req.text(); + return JSON.parse(stripBom(text)) as T; +} + +async function getKVJson(ns: KVNamespace, key: string): Promise { + const raw = await ns.get(key, "text"); + if (raw === null) return null; + return JSON.parse(stripBom(raw)) as T; +} + function cors(env: Env) { return { "Access-Control-Allow-Origin": env.ALLOWED_ORIGIN, @@ -108,6 +125,12 @@ function getCookie(req: Request, name: string): string | null { return m ? decodeURIComponent(m[1]) : null; } +function getBearerToken(req: Request): string | null { + const auth = req.headers.get("Authorization") || ""; + const m = auth.match(/^Bearer\s+(.+)$/i); + return m ? m[1].trim() : null; +} + function setCookie( name: string, val: string, @@ -147,7 +170,7 @@ const SESSION_COOKIE = "sess"; async function getSession(env: Env, token: string | null) { if (!token) return null; - return env.USERS.get(SESSION_PREFIX + token, "json") as Promise; + return getKVJson<{ username: string; exp: number; roles?: string[] }>(env.USERS, SESSION_PREFIX + token); } async function createSession(env: Env, username: string, roles: string[] = []) { @@ -174,7 +197,7 @@ function readTrialCookie(req: Request): number { // ------------------------- async function handleLogin(req: Request, env: Env) { try { - const body = await req.json<{ username?: string; password?: string; _hp?: string }>(); + const body = await parseJsonBody<{ username?: string; password?: string; _hp?: string }>(req); // Honeypot: bots fill hidden fields, humans don't if (body._hp) return bad({ error: "invalid_request" }, env, 400); @@ -186,15 +209,15 @@ async function handleLogin(req: Request, env: Env) { // Accepts both formats make-user.mjs can produce: // Format A: hash = "pbkdf2$sha256$120000$$" (combined string) // Format B: { salt, hash, iterations } as separate fields (older format) - const doc = await env.USERS.get("user:" + username, "json") as null | { + const doc = await getKVJson<{ username: string; - hash: string; // either combined string OR just the derived hash - salt?: string; // present in Format B only - iterations?: number; // present in Format B only + hash: string; + salt?: string; + iterations?: number; expires?: string; roles?: string[]; status?: string; - }; + }>(env.USERS, "user:" + username); if (!doc) return bad({ error: "invalid_credentials" }, env, 401); @@ -230,18 +253,24 @@ async function handleLogin(req: Request, env: Env) { const derived = await hashPasswordPBKDF2(password, saltB64, iterations); if (derived !== storedHash) return bad({ error: "invalid_credentials" }, env, 401); - const session = await createSession(env, username, doc.roles ?? []); - const cookie = setCookie(SESSION_COOKIE, session.token, { httpOnly: true, sameSite: "None", secure: true, maxAge: SESSION_TTL_SECONDS }); + const session = await createSession(env, username, doc.roles ?? []); + const cookie = setCookie(SESSION_COOKIE, session.token, { httpOnly: true, sameSite: "None", secure: true, maxAge: SESSION_TTL_SECONDS }); const clearTrial = setCookie(TRIAL_COOKIE, "0", { httpOnly: true, sameSite: "None", secure: true, maxAge: 0 }); - return ok({ ok: true, username }, env, { "Set-Cookie": `${cookie}, ${clearTrial}` }); + const headers = new Headers({ ...JSON_HEADER, ...cors(env) }); + headers.append("Set-Cookie", cookie); + headers.append("Set-Cookie", clearTrial); + return new Response( + JSON.stringify({ ok: true, username, token: session.token }), + { status: 200, headers } + ); } catch (e: any) { return bad({ error: e?.message || "bad_request" }, env, 400); } } async function handleLogout(req: Request, env: Env) { - const token = getCookie(req, SESSION_COOKIE); + const token = getCookie(req, SESSION_COOKIE) ?? getBearerToken(req); await destroySession(env, token); const clear = setCookie(SESSION_COOKIE, "", { httpOnly: true, sameSite: "None", secure: true, maxAge: 0 }); return ok({ ok: true }, env, { "Set-Cookie": clear }); @@ -262,7 +291,8 @@ const IP_TRIAL_LIMIT = 2; // matches TRIAL_LIMIT async function guardExtract( req: Request, env: Env ): Promise<{ allowed: boolean; mode?: AuthResult; headers?: Record; error?: Response }> { - const sess = await getSession(env, getCookie(req, SESSION_COOKIE)); + const sessionToken = getCookie(req, SESSION_COOKIE) ?? getBearerToken(req); + const sess = await getSession(env, sessionToken); if (sess && sess.exp > Math.floor(Date.now() / 1000)) { return { allowed: true, mode: { kind: "session", username: sess.username, roles: sess.roles ?? [] } }; } @@ -275,7 +305,7 @@ async function guardExtract( const ipKey = IP_TRIAL_PREFIX + ip; let ipUsed = 0; try { - const ipData = await env.USERS.get(ipKey, "json") as null | { count: number }; + const ipData = await getKVJson<{ count: number }>(env.USERS, ipKey); ipUsed = ipData?.count ?? 0; } catch { ipUsed = 0; } @@ -358,7 +388,33 @@ async function extractWithGemini(env: Env, parts: { imgs: string[]; docText: str } // ------------------------- -// 5) /api/extract handler +// 5) /api/sync-chat-history handler +// ------------------------- +async function handleSyncChatHistory(req: Request, env: Env) { + try { + // Verify session (optional but recommended) + const sessionCookie = req.headers.get("cookie"); + + const body = await parseJsonBody(req); + if (!body || typeof body !== "object") { + return bad({ error: "invalid_payload" }, env, 400); + } + + // Store in KV with expiration (7 days) + const key = `chat-history`; + await env.USERS.put(key, JSON.stringify(body), { + expirationTtl: 604800 // 7 days in seconds + }); + + return ok({ ok: true, message: "Chat history synced" }, env); + } catch (err) { + console.error("[sync-chat-history] Error:", err); + return bad({ error: "sync_failed", detail: String(err) }, env, 500); + } +} + +// ------------------------- +// 6) /api/extract handler // ------------------------- async function handleExtract(req: Request, env: Env) { const guard = await guardExtract(req, env); @@ -401,7 +457,7 @@ async function handleExtract(req: Request, env: Env) { } // ------------------------- -// 6) Router +// 7) Router // ------------------------- export default { async fetch(req: Request, env: Env): Promise { @@ -412,9 +468,10 @@ export default { const { pathname } = new URL(req.url); try { - if (pathname === "/api/login" && req.method === "POST") return handleLogin(req, env); - if (pathname === "/api/logout" && req.method === "POST") return handleLogout(req, env); - if (pathname === "/api/extract" && req.method === "POST") return handleExtract(req, env); + if (pathname === "/api/login" && req.method === "POST") return handleLogin(req, env); + if (pathname === "/api/logout" && req.method === "POST") return handleLogout(req, env); + if (pathname === "/api/extract" && req.method === "POST") return handleExtract(req, env); + if (pathname === "/api/sync-chat-history" && req.method === "POST") return handleSyncChatHistory(req, env); return bad({ error: "not_found" }, env, 404); } catch (err: any) { diff --git a/tools/make-user.mjs b/tools/make-user.mjs index a166f9d..906089b 100644 --- a/tools/make-user.mjs +++ b/tools/make-user.mjs @@ -22,7 +22,7 @@ const expires = arg("expires", "2099-12-31"); // PBKDF2 params (browser-compatible & light for Workers) const salt = randomBytes(16); -const iterations = 120000; +const iterations = 100000; const keyLen = 32; const digest = "sha256"; diff --git a/tools/write-user.mjs b/tools/write-user.mjs index 59baa40..3595636 100644 --- a/tools/write-user.mjs +++ b/tools/write-user.mjs @@ -31,7 +31,7 @@ if (!username || !password) { // PBKDF2 hash — same algorithm the worker uses to verify const salt = randomBytes(16); -const iterations = 120000; +const iterations = 100000; const keyLen = 32; const digest = "sha256"; const derived = pbkdf2Sync(password, salt, iterations, keyLen, digest); From 506a586c0e31ab9731c6fd828f56c085d2dcc7ca Mon Sep 17 00:00:00 2001 From: Deep7285 Date: Tue, 28 Apr 2026 20:28:46 +0530 Subject: [PATCH 02/11] add: chat sync infrastructure for web-to-terminal context - CHAT_SYNC_SETUP.md / QUICK_START.md: full docs for the sync workflow - chat-history.json: placeholder file for storing web chat conversations - frontend/chat-sync.js: captures chat messages and syncs to localStorage and backend KV via /api/sync-chat-history - claude-with-context.ps1: PowerShell launcher that loads chat history and starts Claude CLI with full conversation context preloaded - claude-terminal-client.mjs: cross-platform Node.js equivalent launcher Co-Authored-By: Claude Sonnet 4.6 --- CHAT_SYNC_SETUP.md | 307 +++++++++++++++++++++++++++++++++++++ QUICK_START.md | 118 ++++++++++++++ chat-history.json | 24 +++ claude-terminal-client.mjs | 155 +++++++++++++++++++ claude-with-context.ps1 | 77 ++++++++++ frontend/chat-sync.js | 150 ++++++++++++++++++ 6 files changed, 831 insertions(+) create mode 100644 CHAT_SYNC_SETUP.md create mode 100644 QUICK_START.md create mode 100644 chat-history.json create mode 100644 claude-terminal-client.mjs create mode 100644 claude-with-context.ps1 create mode 100644 frontend/chat-sync.js diff --git a/CHAT_SYNC_SETUP.md b/CHAT_SYNC_SETUP.md new file mode 100644 index 0000000..922a6e0 --- /dev/null +++ b/CHAT_SYNC_SETUP.md @@ -0,0 +1,307 @@ +# 🔗 Web-to-Terminal Chat Sync Guide + +## Overview + +This system allows you to: +1. **Capture chat conversations from your web interface** +2. **Save them to a JSON file** automatically +3. **Load that history into Claude CLI in terminal** with full context +4. **Continue debugging/fixing issues** using Claude in the terminal with web chat context + +--- + +## Setup Steps + +### Step 1: Add Chat Sync to Your Web Interface + +Add this script tag to your `index.html` **before** closing ``: + +```html + + +``` + +### Step 2: Capture Chat Messages + +Modify your web interface to feed messages into the chat history. Here are different approaches: + +#### Option A: Manual Logging (Simplest) + +If you have a chat display element, add this after each message appears: + +```javascript +// After a user message appears: +chatHistory.addMessage("user", "User's message text here"); + +// After Claude responds: +chatHistory.addMessage("assistant", "Claude's response here"); +``` + +#### Option B: Observe DOM Changes (Automatic) + +The `chat-sync.js` already includes a `hookChatDisplay()` function. Update the selectors to match your DOM: + +```javascript +// In your index.html or where you display messages: +const messageEl = document.createElement('div'); +messageEl.setAttribute('data-message', 'true'); +messageEl.setAttribute('data-role', 'user'); // or 'assistant' +messageEl.textContent = "Your message"; +chatContainer.appendChild(messageEl); +``` + +#### Option C: Intercept API Calls (Most Elegant) + +Hook into your fetch/API calls: + +```javascript +// Wrap your original API call +const originalFetch = window.fetch; +window.fetch = function(...args) { + return originalFetch.apply(this, args).then(response => { + // If this is a chat API, capture the message + if (args[0].includes('/chat')) { + response.clone().json().then(data => { + if (data.message) { + chatHistory.addMessage("assistant", data.message); + } + }); + } + return response; + }); +}; +``` + +### Step 3: Export Chat History + +Once messages are captured, they automatically save to localStorage. To export to file: + +```javascript +// In browser console: +const historyJson = JSON.stringify(chatHistory.history, null, 2); +console.log(historyJson); + +// Then copy and save to chat-history.json in your project root +``` + +Or manually trigger sync: + +```javascript +chatHistory.syncToFile(); // Sends to backend API +``` + +--- + +## Using Claude CLI with Web Chat Context + +### Method 1: PowerShell Script (Windows) + +**Simple usage:** +```powershell +.\claude-with-context.ps1 +``` + +This loads chat history and starts Claude CLI with the context preloaded. + +**With a specific query:** +```powershell +.\claude-with-context.ps1 -Query "Why is the invoice extraction failing on PDFs?" +``` + +### Method 2: Node.js Script (Cross-platform) + +**Interactive mode:** +```bash +node claude-terminal-client.mjs +``` + +**With a query:** +```bash +node claude-terminal-client.mjs --query "How do I fix the GSTIN extraction?" +``` + +### Method 3: Direct Terminal Command + +If you have Claude CLI installed: + +```bash +# Load history as context +cat chat-history.json | claude "Here's my chat history. Based on this, help me fix..." +``` + +--- + +## Workflow Example + +### Scenario: Debugging Invoice Extraction + +1. **In Web Interface** (while troubleshooting): + ``` + User: "Why are invoices failing to extract?" + Claude: "Could be a PDF encoding issue. Check the error logs." + User: "Got it, let me look at the logs" + Claude: "Great. Try extracting a test PDF with verbose logging enabled." + ``` + +2. **Switch to Terminal** (with full context): + ```powershell + .\claude-with-context.ps1 -Query "I found the issue in worker.ts line 331. How should I fix it?" + ``` + +3. **Claude in Terminal** sees entire conversation history and helps you: + - Understand the issue in context + - Suggest specific code fixes + - Test the changes + - Verify the solution + +--- + +## File Locations + +- **Chat history:** `chat-history.json` (project root) +- **Web sync script:** `frontend/chat-sync.js` +- **PowerShell launcher:** `claude-with-context.ps1` +- **Node.js launcher:** `claude-terminal-client.mjs` +- **Backend sync endpoint:** `src/worker.ts` → `/api/sync-chat-history` + +--- + +## API Reference + +### Backend Endpoint + +**POST `/api/sync-chat-history`** + +Saves chat history to Cloudflare KV storage for persistent access. + +**Request:** +```json +{ + "version": "1.0", + "conversations": [ + { + "id": "conv-1", + "timestamp": "2026-04-28T...", + "platform": "web", + "messages": [ + { + "role": "user", + "content": "Message text", + "timestamp": "2026-04-28T..." + } + ] + } + ], + "lastUpdated": "2026-04-28T...", + "activeConversationId": "conv-1" +} +``` + +**Response:** +```json +{ + "ok": true, + "message": "Chat history synced" +} +``` + +--- + +## Troubleshooting + +### Chat history not appearing in terminal + +1. Check `chat-history.json` exists in project root: + ```bash + ls chat-history.json + ``` + +2. Verify it has content: + ```bash + cat chat-history.json + ``` + +3. If empty, manually capture messages: + ```javascript + chatHistory.addMessage("user", "Your message"); + chatHistory.addMessage("assistant", "Response"); + chatHistory.saveHistory(); + ``` + +### Claude not starting in terminal + +- Install Claude CLI: `npm install -g claude-cli` +- Ensure you have `claude` command available: + ```bash + which claude + ``` + +### PowerShell script permission denied + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned +.\claude-with-context.ps1 +``` + +--- + +## Advanced Usage + +### Get formatted context without starting Claude + +```javascript +// In browser console: +console.log(chatHistory.getFormattedContext()); +``` + +### Export as Markdown + +```javascript +// Create a markdown file with the chat history +const md = chatHistory.getAsMarkdown(); +// Save as chat-history.md +``` + +### Multiple Conversations + +The system supports multiple conversations: + +```javascript +// Get a specific conversation +const conv = chatHistory.history.conversations[0]; + +// Switch active conversation +chatHistory.history.activeConversationId = "conv-2"; +chatHistory.saveHistory(); +``` + +### Programmatic Access from Terminal + +```javascript +// claude-terminal-client.mjs automatically: +// 1. Loads chat-history.json +// 2. Formats all messages as context +// 3. Passes to Claude CLI +// 4. Keeps stdin open for your queries +``` + +--- + +## Security Notes + +- ✅ Chat history stored locally (no external uploads unless you sync) +- ✅ Backend KV storage auto-expires after 7 days +- ✅ All API calls use httpOnly cookies for auth +- ⚠️ Don't store sensitive API keys in chat history +- ⚠️ Review history before sharing with others + +--- + +## Next Steps + +1. Add `chat-sync.js` to your `index.html` +2. Set up message capture (choose Option A, B, or C above) +3. Test by running: `.\claude-with-context.ps1` +4. Deploy changes to Cloudflare Workers + +Good luck with your invoice extraction debugging! 🚀 diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..b35feed --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,118 @@ +# 🚀 Quick Start — Chat Sync + +## For Immediate Use + +### 1. Load chat history + start Claude CLI + +**Windows (PowerShell):** +```powershell +.\claude-with-context.ps1 +``` + +**macOS/Linux:** +```bash +node claude-terminal-client.mjs +``` + +### 2. Load chat history + ask a question + +**Windows:** +```powershell +.\claude-with-context.ps1 -Query "Help me debug this issue" +``` + +**macOS/Linux:** +```bash +node claude-terminal-client.mjs --query "Help me debug this issue" +``` + +--- + +## Integration with Your Web App + +### Add to index.html (before ``): +```html + +``` + +### Capture messages from your chat UI: +```javascript +// After user sends a message: +chatHistory.addMessage("user", userMessageText); + +// After Claude responds: +chatHistory.addMessage("assistant", responseText); +``` + +--- + +## File Structure Created + +``` +invoice-worker/ +├── chat-history.json ← Stores conversation history +├── CHAT_SYNC_SETUP.md ← Full documentation (you're reading the quick version) +├── QUICK_START.md ← This file +├── claude-with-context.ps1 ← PowerShell launcher (Windows) +├── claude-terminal-client.mjs ← Node.js launcher (any OS) +├── frontend/ +│ └── chat-sync.js ← Web capture + storage logic +└── src/ + └── worker.ts ← Updated with /api/sync-chat-history endpoint +``` + +--- + +## How It Works + +1. **Web Interface** → Messages saved to `chat-history.json` and browser localStorage +2. **Terminal Script** → Loads `chat-history.json` as context +3. **Claude CLI** → Starts with all conversation history preloaded +4. **You Debug** → Claude understands full context from web chat + +--- + +## Example Workflow + +``` +[Web Chat] +You: "Why is GSTIN extraction failing?" +Claude: "Check the regex pattern in schema_and_prompt.ts" + +[Terminal] +$ .\claude-with-context.ps1 -Query "I found the issue in line 45. How to fix?" + +[Claude Terminal - with web context loaded] +Claude: "Based on our discussion, the issue is... + Here's the fix for line 45 in schema_and_prompt.ts..." +``` + +--- + +## Requirements + +- **Claude CLI** installed: `npm install -g claude-cli` (or use `claude` command) +- **Node.js** (for .mjs script) +- **PowerShell 5+** (Windows) or bash (macOS/Linux) + +--- + +## Troubleshooting + +**"Chat history not found" error:** +1. Make sure you've added messages to the chat +2. Verify `chat-history.json` exists in project root +3. Run: `node claude-terminal-client.mjs` to verify it can load + +**Claude not starting:** +1. Check Claude CLI is installed: `which claude` +2. Test manually: `echo "Hello" | claude` + +**Messages not being captured:** +1. Open browser DevTools → Console +2. Run: `chatHistory.addMessage("user", "test")` +3. Verify file updates: `cat chat-history.json` + +--- + +For detailed setup and advanced usage, see **CHAT_SYNC_SETUP.md** diff --git a/chat-history.json b/chat-history.json new file mode 100644 index 0000000..a4a8bfd --- /dev/null +++ b/chat-history.json @@ -0,0 +1,24 @@ +{ + "version": "1.0", + "conversations": [ + { + "id": "conv-1", + "timestamp": "2026-04-28T00:00:00Z", + "platform": "web", + "messages": [ + { + "role": "user", + "content": "Start your conversation here...", + "timestamp": "2026-04-28T00:00:00Z" + }, + { + "role": "assistant", + "content": "Ready to help fix invoice extraction issues.", + "timestamp": "2026-04-28T00:00:01Z" + } + ] + } + ], + "lastUpdated": "2026-04-28T00:00:00Z", + "activeConversationId": "conv-1" +} diff --git a/claude-terminal-client.mjs b/claude-terminal-client.mjs new file mode 100644 index 0000000..df0483b --- /dev/null +++ b/claude-terminal-client.mjs @@ -0,0 +1,155 @@ +#!/usr/bin/env node +// claude-terminal-client.mjs — CLI to load web chat history and interact with Claude +// Usage: node claude-terminal-client.mjs [--query "your question"] + +import fs from "fs"; +import path from "path"; +import readline from "readline"; +import { fileURLToPath } from "url"; +import { spawn } from "child_process"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CHAT_HISTORY_PATH = path.join(__dirname, "chat-history.json"); + +// Color codes for terminal +const colors = { + reset: "\x1b[0m", + green: "\x1b[32m", + cyan: "\x1b[36m", + yellow: "\x1b[33m", + red: "\x1b[31m", + gray: "\x1b[90m", + bold: "\x1b[1m" +}; + +function log(msg, color = "reset") { + console.log(`${colors[color]}${msg}${colors.reset}`); +} + +async function loadChatHistory() { + if (!fs.existsSync(CHAT_HISTORY_PATH)) { + log(`⚠️ Chat history not found at ${CHAT_HISTORY_PATH}`, "yellow"); + return null; + } + + try { + const data = fs.readFileSync(CHAT_HISTORY_PATH, "utf8"); + const history = JSON.parse(data); + return history; + } catch (err) { + log(`✗ Failed to load chat history: ${err.message}`, "red"); + return null; + } +} + +function formatHistoryAsContext(history) { + if (!history || !history.conversations || history.conversations.length === 0) { + return ""; + } + + let activeConv = history.conversations.find(c => c.id === history.activeConversationId); + if (!activeConv) { + activeConv = history.conversations[0]; + } + + let context = `## 📋 Chat History from Web Interface\n`; + context += `**Last Updated:** ${history.lastUpdated}\n`; + context += `**Conversation:** ${activeConv.id}\n`; + context += `**Messages:** ${activeConv.messages.length}\n\n`; + context += `---\n\n`; + + activeConv.messages.forEach((msg, idx) => { + const roleUpper = msg.role.toUpperCase(); + context += `**[${idx + 1}] ${roleUpper}:**\n\n${msg.content}\n\n---\n\n`; + }); + + return context; +} + +async function runClaudeWithContext(query = null) { + // Load chat history + const history = await loadChatHistory(); + let context = history ? formatHistoryAsContext(history) : ""; + + if (history) { + const activeConv = history.conversations.find(c => c.id === history.activeConversationId) || history.conversations[0]; + log(`✓ Loaded chat history (${activeConv.messages.length} messages)`, "green"); + } else { + log(`ℹ No previous chat history`, "gray"); + } + + log(`---`, "gray"); + + // If query provided, process it with context + if (query) { + const fullPrompt = context ? `${context}\n\n---\n\nNEW QUESTION:\n\n${query}` : query; + log(`▶ Processing query with context...`, "cyan"); + log(``, "reset"); + + // Call Claude with the full prompt + const claudeProcess = spawn("claude", [], { + stdio: "inherit", + shell: true + }); + + claudeProcess.stdin.write(fullPrompt); + claudeProcess.stdin.end(); + + return new Promise((resolve, reject) => { + claudeProcess.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Claude process exited with code ${code}`)); + } + }); + }); + } + + // Interactive mode + log(`💬 Starting Claude CLI with loaded context...`, "cyan"); + log(`Type 'exit' or Ctrl+C to quit\n`, "gray"); + + // Start Claude interactively + const claudeProcess = spawn("claude", [], { + stdio: "inherit", + shell: true + }); + + // Send context if available + if (context) { + claudeProcess.stdin.write(context); + claudeProcess.stdin.write("\n\n---\n\nContext loaded. Ready to assist with invoice extraction issues.\n\n"); + } + + return new Promise((resolve, reject) => { + claudeProcess.on("close", (code) => { + if (code === 0 || code === null) { + resolve(); + } else { + reject(new Error(`Claude process exited with code ${code}`)); + } + }); + + claudeProcess.on("error", (err) => { + reject(err); + }); + }); +} + +// Parse CLI arguments +const args = process.argv.slice(2); +let query = null; + +for (let i = 0; i < args.length; i++) { + if (args[i] === "--query" && args[i + 1]) { + query = args[i + 1]; + break; + } +} + +// Run +runClaudeWithContext(query).catch(err => { + log(`✗ Error: ${err.message}`, "red"); + process.exit(1); +}); diff --git a/claude-with-context.ps1 b/claude-with-context.ps1 new file mode 100644 index 0000000..649c016 --- /dev/null +++ b/claude-with-context.ps1 @@ -0,0 +1,77 @@ +# claude-with-context.ps1 — Load chat history and run Claude in terminal +# Usage: .\claude-with-context.ps1 [query] + +param( + [string]$Query = "" +) + +# Path to chat history +$chatHistoryPath = "$(Get-Location)\chat-history.json" + +# Check if history exists +if (!(Test-Path $chatHistoryPath)) { + Write-Host "⚠️ Chat history not found at: $chatHistoryPath" -ForegroundColor Yellow + Write-Host "Starting fresh Claude session..." -ForegroundColor Gray + if ($Query) { + claude $Query + } else { + claude + } + exit +} + +# Load history +try { + $historyJson = Get-Content $chatHistoryPath -Raw | ConvertFrom-Json + Write-Host "✓ Loaded chat history from web interface" -ForegroundColor Green +} catch { + Write-Host "✗ Failed to parse chat history: $_" -ForegroundColor Red + exit 1 +} + +# Extract active conversation +$activeConvId = $historyJson.activeConversationId +$activeConv = $historyJson.conversations | Where-Object { $_.id -eq $activeConvId } | Select-Object -First 1 + +if ($null -eq $activeConv) { + $activeConv = $historyJson.conversations | Select-Object -First 1 +} + +# Format history for Claude context +$contextString = @" +## Previous Chat Context from Web Interface + +**Last Updated:** $($historyJson.lastUpdated) +**Conversation ID:** $($activeConv.id) + +--- + +"@ + +$activeConv.messages | ForEach-Object { + $roleDisplay = $_.role.ToUpper() + $contextString += "`n**$roleDisplay**:`n$($_.content)`n`n---`n" +} + +# Create temporary file with context +$tempContextFile = "$env:TEMP\claude_context_$(Get-Random).txt" +$contextString | Out-File $tempContextFile -Encoding UTF8 + +Write-Host "`n📋 Chat context loaded ($($activeConv.messages.Count) messages)" -ForegroundColor Cyan +Write-Host "---" -ForegroundColor Gray + +# If query provided, append to context and run directly +if ($Query) { + Write-Host "`n▶ Running query with context..." -ForegroundColor Cyan + $fullPrompt = $contextString + "`n\nNEW QUERY:\n$Query" + $fullPrompt | claude +} else { + Write-Host "`n💬 Starting Claude CLI with loaded context..." -ForegroundColor Cyan + Write-Host "Type 'exit' or Ctrl+C to quit`n" -ForegroundColor Gray + + # Run Claude interactively + $contextString | claude +} + +# Cleanup +Remove-Item $tempContextFile -ErrorAction SilentlyContinue diff --git a/frontend/chat-sync.js b/frontend/chat-sync.js new file mode 100644 index 0000000..5a22e9f --- /dev/null +++ b/frontend/chat-sync.js @@ -0,0 +1,150 @@ +// chat-sync.js — Sync web chat with terminal/local storage +// Saves chat messages to local JSON file that Claude CLI can load + +class ChatHistoryManager { + constructor() { + this.storageKey = "invoice_chat_history"; + this.localFilePath = "../chat-history.json"; // Relative to frontend + this.history = this.loadHistory(); + } + + loadHistory() { + const stored = localStorage.getItem(this.storageKey); + if (stored) { + try { + return JSON.parse(stored); + } catch (e) { + console.warn("Failed to parse chat history:", e); + } + } + return this.getEmptyHistory(); + } + + getEmptyHistory() { + return { + version: "1.0", + conversations: [ + { + id: this.generateId(), + timestamp: new Date().toISOString(), + platform: "web", + messages: [] + } + ], + lastUpdated: new Date().toISOString(), + activeConversationId: "" + }; + } + + generateId() { + return `conv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + addMessage(role, content, conversationId = null) { + const activeConvId = conversationId || this.history.activeConversationId || this.history.conversations[0]?.id; + + let conversation = this.history.conversations.find(c => c.id === activeConvId); + if (!conversation) { + conversation = { + id: this.generateId(), + timestamp: new Date().toISOString(), + platform: "web", + messages: [] + }; + this.history.conversations.push(conversation); + this.history.activeConversationId = conversation.id; + } + + conversation.messages.push({ + role, + content, + timestamp: new Date().toISOString() + }); + + this.history.lastUpdated = new Date().toISOString(); + this.saveHistory(); + } + + saveHistory() { + localStorage.setItem(this.storageKey, JSON.stringify(this.history, null, 2)); + this.syncToFile(); + } + + syncToFile() { + // Send to server endpoint to write to file + fetch("../api/sync-chat-history", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(this.history) + }).catch(err => console.warn("Failed to sync to file:", err)); + } + + getFormattedContext() { + // Format history for Claude CLI context + const activeConv = this.history.conversations.find( + c => c.id === this.history.activeConversationId + ) || this.history.conversations[0]; + + if (!activeConv) return ""; + + return activeConv.messages + .map(msg => `${msg.role.toUpperCase()}:\n${msg.content}`) + .join("\n\n---\n\n"); + } + + getAsMarkdown() { + // Export as markdown for terminal use + const activeConv = this.history.conversations.find( + c => c.id === this.history.activeConversationId + ) || this.history.conversations[0]; + + if (!activeConv) return ""; + + let md = `# Chat History\n\n**Exported:** ${new Date().toISOString()}\n\n`; + activeConv.messages.forEach((msg, idx) => { + md += `## Message ${idx + 1} (${msg.role})\n\n${msg.content}\n\n`; + }); + return md; + } + + exportToTerminal() { + return this.getFormattedContext(); + } +} + +// Global instance +const chatHistory = new ChatHistoryManager(); + +// Hook into existing functionality (if you have message display) +function hookChatDisplay() { + // Example: If you have chat messages displayed, add observer + // This captures messages as they appear on screen + const observer = new MutationObserver((mutations) => { + mutations.forEach(mutation => { + // Look for message elements and extract text + mutation.addedNodes.forEach(node => { + if (node.nodeType === 1) { // Element node + // Adapt selectors to your actual DOM structure + const messageEl = node.closest("[data-message]"); + if (messageEl) { + const role = messageEl.getAttribute("data-role") || "user"; + const content = messageEl.textContent; + chatHistory.addMessage(role, content); + } + } + }); + }); + }); + + // Observe chat container (adjust selector to your DOM) + const chatContainer = document.getElementById("chat-container") || document.body; + observer.observe(chatContainer, { + childList: true, + subtree: true, + characterData: true + }); +} + +// Initialize on load +document.addEventListener("DOMContentLoaded", hookChatDisplay); From a7433b3d45407f5813d024c9ddb9aac618356229 Mon Sep 17 00:00:00 2001 From: Deep7285 Date: Tue, 28 Apr 2026 20:44:01 +0530 Subject: [PATCH 03/11] feat: add password visibility toggle and highlight footer contact links - Login modal: eye icon button on password field toggles between hidden/visible; icon swaps between open-eye and slashed-eye SVG - Footer: Email and LinkedIn links now styled with accent-colored border, glow ring, and accent text so they stand out for visitors to reach out Co-Authored-By: Claude Sonnet 4.6 --- index.html | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index a5f5ed4..f5d121a 100644 --- a/index.html +++ b/index.html @@ -459,6 +459,15 @@ } .field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); } .field-hp { display: none !important; } + .field-pw-wrap { position: relative; } + .field-pw-wrap input { padding-right: 42px; } + .btn-pw-toggle { + position: absolute; right: 10px; top: 50%; transform: translateY(-50%); + background: none; border: none; cursor: pointer; color: var(--text-muted); + padding: 4px; display: flex; align-items: center; transition: color .15s; + } + .btn-pw-toggle:hover { color: var(--text-dim); } + .btn-pw-toggle svg { width: 18px; height: 18px; } .login-err { background: rgba(232,70,70,.1); border: 1px solid rgba(232,70,70,.25); border-radius: 8px; padding: 10px 14px; font-size: 13px; color: #EF8888; margin-bottom: 16px; } .modal-acts { display: flex; gap: 10px; justify-content: flex-end; margin-top: 8px; } .btn-cancel { padding: 10px 20px; background: transparent; color: var(--text-dim); border: 1px solid var(--border-hi); border-radius: 10px; font-weight: 600; cursor: pointer; font-size: 14px; } @@ -493,11 +502,13 @@ .foot-links { display: flex; align-items: center; gap: 10px; } .foot-link { display: inline-flex; align-items: center; gap: 7px; - padding: 7px 14px; background: var(--surface-2); - border: 1px solid var(--border); border-radius: 8px; - font-size: 13px; font-weight: 600; color: var(--text-dim); transition: all .2s var(--ease); + padding: 7px 16px; background: var(--surface-2); + border: 1px solid var(--accent); border-radius: 8px; + font-size: 13px; font-weight: 600; color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-dim); + transition: all .2s var(--ease); } - .foot-link:hover { color: var(--text); border-color: var(--border-hi); text-decoration: none; } + .foot-link:hover { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); box-shadow: var(--acc-glow); text-decoration: none; } .btn-theme { width: 36px; height: 36px; border-radius: 8px; @@ -741,7 +752,14 @@

Three steps, zero friction

- +
+ + +