# GEO Agent Article Optimizer

Local MVP for optimizing pasted GEO-related articles while preserving confirmed
brand facts. The app extracts a fact card, requires user confirmation, rewrites
under those constraints, runs QA gates, and exports Markdown, Word, and JSON.

## Setup

```bash
npm install
cp .env.example .env.local
npm run dev
```

Open `http://localhost:3000`.

## Environment

Required for live model calls:

```text
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-pro
DEEPSEEK_THINKING=disabled
APP_DATA_DIR=./data
API_ACCESS_KEY=local-dev-key
API_AUTH_DISABLED=false
```

OpenAI-compatible fallback keys are also accepted:

```text
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4.1-mini
```

When a DeepSeek or OpenAI-compatible key is configured, the workflow uses the
provider for fact extraction, article optimization, QA enrichment, and targeted
rewrite. Every LLM response is validated with Zod before use. When no API key is
configured, or when the provider response is invalid, deterministic local
fallbacks keep the workflow usable for tests and local review.

All API requests require the configured access key:

```text
x-api-key: <API_ACCESS_KEY>
```

## LLM Runtime Logs

LLM calls emit regular server-side logs from `src/lib/llm/client.ts`. These logs
are intended for local development, customer demos, and production
troubleshooting.

Example:

```text
[llm:start] provider=deepseek model=deepseek-v4-pro task=article_optimizer
[llm:response] task=article_optimizer duration_ms=18342 raw={"title":"..."}
[llm:validated] task=article_optimizer ok=true
```

The `raw=` field is truncated to 4000 characters to avoid log explosions. Set
`LLM_LOG_RAW_LIMIT=0` to suppress raw response snippets, or set a larger number
when diagnosing model output. API keys, prompts, system messages, and
environment secrets are not logged.

Workflow task names are:

- `fact_extractor`
- `article_optimizer`
- `quality_inspector`
- `targeted_rewriter`

## Commands

```bash
npm test
npm run build
npx playwright test
```

## Cloudflare Workers Deployment

Cloudflare deployment is manual. Pushing to Git does not deploy or hot-update
the production Worker.

Local development:

```bash
npm run dev
```

Cloudflare preview:

```bash
cp .dev.vars.example .dev.vars
npm run d1:migrate:local
npm run preview:worker
```

Create private staging resources:

```bash
npx wrangler d1 create geo-agent-article-optimizer-staging
npx wrangler r2 bucket create geo-agent-article-optimizer-staging
```

Create private production resources:

```bash
npx wrangler d1 create geo-agent-article-optimizer-production
npx wrangler r2 bucket create geo-agent-article-optimizer-production
```

After D1 creation, copy the returned database IDs into the matching
`wrangler.jsonc` environment entries. Keep R2 buckets private; do not add public
bucket domains.

Set secrets:

```bash
npx wrangler secret put API_ACCESS_KEY --env staging
npx wrangler secret put DEEPSEEK_API_KEY --env staging
npx wrangler secret put API_ACCESS_KEY --env production
npx wrangler secret put DEEPSEEK_API_KEY --env production
```

D1 schema changes only through migrations. Runtime code must not rebuild or
clear production tables. Apply migrations in this order:

```bash
npm run d1:migrate:local
npm run d1:migrate:staging
npm run deploy:worker:staging
npm run d1:migrate:production
npm run deploy:worker:production
```

## FRP Mainland Access Tunnel

The Cloudflare staging URL can be unreachable from mainland networks. To expose
the local project through an FRP server, copy the example configs and fill in the
local server address, auth token, and shared secret:

```bash
cp deploy/frpc.geo-agent-article-optimizer.example.toml deploy/frpc.geo-agent-article-optimizer.toml
cp deploy/frpc.geo-agent-article-optimizer-visitor.example.toml deploy/frpc.geo-agent-article-optimizer-visitor.toml
```

The real `deploy/*.toml` files are ignored by Git so local credentials stay out
of the public repository.

Run the app locally and start frpc with the project config:

```bash
npm run dev
frpc -c deploy/frpc.geo-agent-article-optimizer.toml
```

The project-side frpc config publishes local `127.0.0.1:3000` as the stcp
service `geo_agent_article_optimizer`.

On the mainland access machine, start the visitor config that binds the remote
service to a local port:

```bash
frpc -c deploy/frpc.geo-agent-article-optimizer-visitor.toml
```

Then open `http://127.0.0.1:6009` on that machine.

## Exports

Local generated files are written under:

```text
data/exports/<job_id>/
```

Cloudflare exports are written to the private R2 bucket bound as
`EXPORT_BUCKET` and served only through authenticated API routes.

Each passing or warning-only QA run can produce:

- `optimized.md`
- `optimized.docx`
- `qa_report.json`

Hard QA failures block export.

## MVP Limits

- Pasted text only; no direct `.docx` parsing.
- Local SQLite by default; Cloudflare deployments use D1.
- No account permissions or collaboration.
- No publishing platform APIs.
- No batch queue.
- Basic Word export layout only.
- Unconfirmed facts are never used as truth.
