feat: initial monorepo setup with Next.js landing page
- pnpm workspaces monorepo with apps/ and packages/ - Next.js 16 landing page (apps/web) with dark theme, feature overview - Package stubs: @webproxy/core, @webproxy/indexer, @webproxy/shared - Proxy server placeholder (apps/proxy) - Project spec, architecture docs, and deployment guide - Gitea remote configured at 185.191.239.154:3000 Co-Authored-By: UnicornDev <noreply@unicorndev.wtf>
This commit is contained in:
commit
17bba2d040
358
.claude/RUNBOOK.md
Normal file
358
.claude/RUNBOOK.md
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# UnicornDev Agent Runbook
|
||||||
|
|
||||||
|
> Quick reference for consistent, reliable behavior. Consult this at every phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Start Checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Load .claude/settings.json
|
||||||
|
□ Load .claude/knowledge/index.json
|
||||||
|
□ Note project type and current state
|
||||||
|
□ Check for pending tasks
|
||||||
|
□ Initialize meta-cognition monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Request Classification
|
||||||
|
|
||||||
|
```
|
||||||
|
USER INPUT
|
||||||
|
│
|
||||||
|
├─► TASK (do something)
|
||||||
|
│ ├─ Feature → Spec first?
|
||||||
|
│ ├─ Bug → Debug skill
|
||||||
|
│ ├─ Question → Explore
|
||||||
|
│ └─ Refactor → Plan first
|
||||||
|
│
|
||||||
|
├─► QUESTION (explain something)
|
||||||
|
│ ├─ About code → Read/explore
|
||||||
|
│ ├─ About tech → Check knowledge
|
||||||
|
│ └─ How to → May need to learn
|
||||||
|
│
|
||||||
|
└─► FEEDBACK (response to our work)
|
||||||
|
├─ Approval → Continue
|
||||||
|
├─ Correction → Update, maybe learn
|
||||||
|
└─ Rejection → Stop, understand why
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complexity Assessment (Before Starting Tasks)
|
||||||
|
|
||||||
|
```
|
||||||
|
QUICK COMPLEXITY CHECK:
|
||||||
|
|
||||||
|
Single file + known solution + low risk?
|
||||||
|
├─► YES → TRIVIAL: Just do it
|
||||||
|
└─► NO → Full assessment needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Assessment
|
||||||
|
|
||||||
|
```
|
||||||
|
Rate 1-5 each, then average:
|
||||||
|
|
||||||
|
SCOPE: 1=one function ... 5=multiple systems
|
||||||
|
UNCERTAINTY: 1=known solution ... 5=unprecedented
|
||||||
|
DEPENDENCIES: 1=none ... 5=complex chain
|
||||||
|
RISK: 1=safe ... 5=critical
|
||||||
|
KNOWLEDGE: 1=fully known ... 5=unknown territory
|
||||||
|
|
||||||
|
Score 1.0-1.5: TRIVIAL → Direct execution
|
||||||
|
Score 1.6-2.5: SIMPLE → Light planning
|
||||||
|
Score 2.6-3.5: MODERATE → Task list, verify each
|
||||||
|
Score 3.6-4.5: COMPLEX → Phases, checkpoints, regression tests
|
||||||
|
Score 4.6-5.0: EXTREME → Prove each step, user checkpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Methodology by Level
|
||||||
|
|
||||||
|
```
|
||||||
|
TRIVIAL: Do → Verify → Done
|
||||||
|
SIMPLE: Plan (mental) → Do → Test → Done
|
||||||
|
MODERATE: Tasks → [Do → Verify → Check regression]* → Done
|
||||||
|
COMPLEX: Phases → [Do → Verify → Full regression → Checkpoint]* → Done
|
||||||
|
EXTREME: Prove approach → Phases → [Verify continuously]* → Done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decomposition Rules
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Each increment ≤ 30 minutes
|
||||||
|
□ Each increment independently verifiable
|
||||||
|
□ Each increment revertable
|
||||||
|
□ No increment leaves system broken
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Levels
|
||||||
|
|
||||||
|
```
|
||||||
|
L0: Compiles? (tsc --noEmit, build)
|
||||||
|
L1: Unit works? (test function/component)
|
||||||
|
L2: Integrates? (works with dependencies)
|
||||||
|
L3: End-to-end? (user flow works)
|
||||||
|
L4: No regressions? (existing tests pass)
|
||||||
|
|
||||||
|
TRIVIAL: L0
|
||||||
|
SIMPLE: L0 + L1
|
||||||
|
MODERATE: L0 + L1 + L2 + L4
|
||||||
|
COMPLEX: L0 + L1 + L2 + L3 + L4
|
||||||
|
EXTREME: All levels, continuously
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regression Response
|
||||||
|
|
||||||
|
```
|
||||||
|
IF regression found:
|
||||||
|
1. STOP immediately
|
||||||
|
2. Identify what broke
|
||||||
|
3. Assess severity (critical/major/minor)
|
||||||
|
4. Critical/Major → REVERT, then reassess
|
||||||
|
5. Minor → Quick fix if <5min, else revert
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before Every Action
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Is this the right approach?
|
||||||
|
□ Do I have what I need?
|
||||||
|
□ Will this move us forward?
|
||||||
|
|
||||||
|
If unsure → PAUSE, don't guess
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Every 10 Actions: Meta-Check
|
||||||
|
|
||||||
|
```
|
||||||
|
FLOW STATE CHECK:
|
||||||
|
|
||||||
|
GREEN (continue):
|
||||||
|
✓ Making progress
|
||||||
|
✓ Actions succeeding
|
||||||
|
✓ Clear direction
|
||||||
|
|
||||||
|
YELLOW (caution):
|
||||||
|
⚠ Some uncertainty
|
||||||
|
⚠ Minor retries
|
||||||
|
⚠ Searching without finding
|
||||||
|
→ Pause, assess
|
||||||
|
|
||||||
|
RED (stop):
|
||||||
|
✗ Stuck
|
||||||
|
✗ Repeated failures
|
||||||
|
✗ No clear next action
|
||||||
|
→ Full reflection required
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trigger Quick Reference
|
||||||
|
|
||||||
|
### Learn System
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Unknown package in package.json | Research + document |
|
||||||
|
| Unknown import in code | Research + document |
|
||||||
|
| "How do I use X?" | Check knowledge, learn if missing |
|
||||||
|
| Web search reveals new tech | Document key patterns |
|
||||||
|
|
||||||
|
### Reflect System
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Same error 3x | STOP → Diagnose → Fix |
|
||||||
|
| Same search 3x | Switch to exploration pattern |
|
||||||
|
| No progress 5 actions | Pause → Reflect |
|
||||||
|
| 3+ files without completing any | Focus on one thing |
|
||||||
|
|
||||||
|
### Knowledge System
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Task involves known tech | Load relevant knowledge |
|
||||||
|
| Decision point | Check for relevant patterns |
|
||||||
|
| About to implement | Check for anti-patterns |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision Framework
|
||||||
|
|
||||||
|
### Approach Selection
|
||||||
|
```
|
||||||
|
1. Check knowledge base for patterns
|
||||||
|
2. Consider simplest approach first
|
||||||
|
3. If multiple options, weigh trade-offs
|
||||||
|
4. If uncertain, ask or research
|
||||||
|
5. Document decision reasoning
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blocker vs Optimization
|
||||||
|
```
|
||||||
|
Can I proceed without fixing this?
|
||||||
|
├─► NO (blocker) → Fix now
|
||||||
|
└─► YES → Is slowdown > 50%?
|
||||||
|
├─► YES + quick fix → Fix now
|
||||||
|
└─► Otherwise → Queue for later
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Ask User
|
||||||
|
```
|
||||||
|
ASK when:
|
||||||
|
- Requirements are ambiguous
|
||||||
|
- Multiple valid approaches exist and preference matters
|
||||||
|
- Destructive action needed
|
||||||
|
- Stuck after 3 recovery attempts
|
||||||
|
|
||||||
|
DON'T ASK when:
|
||||||
|
- Answer is in codebase
|
||||||
|
- Standard approach exists
|
||||||
|
- Can make reasonable assumption
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recovery Procedures
|
||||||
|
|
||||||
|
### Search Loop (3+ similar searches)
|
||||||
|
```
|
||||||
|
1. STOP searching
|
||||||
|
2. Read package.json for libraries
|
||||||
|
3. List directory structure
|
||||||
|
4. Find entry points, follow trail
|
||||||
|
5. Use systematic exploration pattern
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Loop (3+ same error)
|
||||||
|
```
|
||||||
|
1. STOP retrying
|
||||||
|
2. Log the error
|
||||||
|
3. Ask: Why is this failing?
|
||||||
|
4. Ask: What assumption is wrong?
|
||||||
|
5. Try different approach
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stuck (no clear next action)
|
||||||
|
```
|
||||||
|
1. STOP everything
|
||||||
|
2. Re-read original request
|
||||||
|
3. What is the smallest next step?
|
||||||
|
4. Do ONLY that step
|
||||||
|
5. Verify it worked before continuing
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quality Gates
|
||||||
|
|
||||||
|
### Before Delivering Code
|
||||||
|
```
|
||||||
|
□ Does it compile/run?
|
||||||
|
□ Does it handle errors?
|
||||||
|
□ Is it typed correctly?
|
||||||
|
□ Does it follow project patterns?
|
||||||
|
□ Does it match the request?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Before Creating Files
|
||||||
|
```
|
||||||
|
□ Is this file necessary?
|
||||||
|
□ Does similar file already exist?
|
||||||
|
□ Is the location correct?
|
||||||
|
□ Is the naming consistent?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Before Completing Task
|
||||||
|
```
|
||||||
|
□ All acceptance criteria met?
|
||||||
|
□ No TODOs left unaddressed?
|
||||||
|
□ Would I approve this PR?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Improvement Protocol
|
||||||
|
|
||||||
|
### After Every Task
|
||||||
|
```
|
||||||
|
1. Quick review: What went well? What didn't?
|
||||||
|
2. If struggled with something → Create pattern
|
||||||
|
3. If learned something new → Add to knowledge
|
||||||
|
4. If found better approach → Document it
|
||||||
|
```
|
||||||
|
|
||||||
|
### Improvement Types
|
||||||
|
| Situation | Create |
|
||||||
|
|-----------|--------|
|
||||||
|
| Recurring task difficulty | .claude/skills/{name}.md |
|
||||||
|
| Missing technology info | .claude/knowledge/{cat}/{name}.md |
|
||||||
|
| Reusable solution | .claude/knowledge/patterns/{name}.md |
|
||||||
|
| Default not working | Update .claude/settings.json |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging Requirements
|
||||||
|
|
||||||
|
### Always Log
|
||||||
|
- Session start/end
|
||||||
|
- Task start/completion
|
||||||
|
- Errors encountered
|
||||||
|
- Reflections triggered
|
||||||
|
- Improvements made
|
||||||
|
- Major decisions
|
||||||
|
|
||||||
|
### Log Location
|
||||||
|
```
|
||||||
|
.claude/logs/
|
||||||
|
├─ session-{date}-{id}.jsonl # Session events
|
||||||
|
├─ reflections.jsonl # Reflection events
|
||||||
|
├─ improvements-queue.jsonl # Queued improvements
|
||||||
|
└─ reviews/ # Post-task reviews
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Emergency Procedures
|
||||||
|
|
||||||
|
### Completely Lost
|
||||||
|
```
|
||||||
|
1. Stop all actions
|
||||||
|
2. Say: "I'm having trouble. Let me reassess."
|
||||||
|
3. Re-read original request
|
||||||
|
4. List what has been done
|
||||||
|
5. Identify the gap
|
||||||
|
6. Take smallest possible next step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breaking Changes Risk
|
||||||
|
```
|
||||||
|
1. Stop before executing
|
||||||
|
2. Warn user explicitly
|
||||||
|
3. Explain what could break
|
||||||
|
4. Wait for confirmation
|
||||||
|
5. Create backup plan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unknown Territory
|
||||||
|
```
|
||||||
|
1. Acknowledge uncertainty
|
||||||
|
2. Research before acting
|
||||||
|
3. Start with smallest experiment
|
||||||
|
4. Verify before scaling
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
1. **Progress over perfection** - Move forward, iterate
|
||||||
|
2. **Detect and correct** - Notice problems early, fix fast
|
||||||
|
3. **Learn and improve** - Every struggle is a learning opportunity
|
||||||
|
4. **When in doubt, stop** - Pausing beats failing forward
|
||||||
|
5. **Simplest path first** - Complexity is earned, not assumed
|
||||||
102
.claude/hooks/qa-after-write.ts
Executable file
102
.claude/hooks/qa-after-write.ts
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* @file qa-after-write.ts
|
||||||
|
* @description Reminds to run QA after file writes
|
||||||
|
*
|
||||||
|
* Runs after Write/Edit to enforce quality gates.
|
||||||
|
* Claude Code passes input via STDIN as JSON.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'fs'
|
||||||
|
import { join, extname } from 'path'
|
||||||
|
|
||||||
|
interface HookInput {
|
||||||
|
tool_name?: string
|
||||||
|
tool_input?: Record<string, unknown>
|
||||||
|
cwd?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectRoot(): string {
|
||||||
|
return process.env.CLAUDE_PROJECT_DIR || process.cwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdin(): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let data = ''
|
||||||
|
process.stdin.setEncoding('utf8')
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
data += chunk
|
||||||
|
}
|
||||||
|
})
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
// Timeout after 1 second if no input
|
||||||
|
setTimeout(() => resolve(data), 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const projectRoot = getProjectRoot()
|
||||||
|
const logsDir = join(projectRoot, '.claude/logs')
|
||||||
|
const qaQueueFile = join(logsDir, 'qa-queue.jsonl')
|
||||||
|
|
||||||
|
mkdirSync(logsDir, { recursive: true })
|
||||||
|
|
||||||
|
// Parse input from Claude Code hook (via stdin)
|
||||||
|
let input: HookInput = {}
|
||||||
|
try {
|
||||||
|
const stdinData = await readStdin()
|
||||||
|
if (stdinData.trim()) {
|
||||||
|
input = JSON.parse(stdinData)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid JSON, continue with empty input
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = input.tool_name || ''
|
||||||
|
const toolInput = input.tool_input || {}
|
||||||
|
const filePath = String(toolInput.file_path || '')
|
||||||
|
|
||||||
|
// Only track code files
|
||||||
|
const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go', '.java']
|
||||||
|
const ext = extname(filePath)
|
||||||
|
|
||||||
|
if ((tool === 'Write' || tool === 'Edit') && codeExtensions.includes(ext)) {
|
||||||
|
// Queue for QA
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
file: filePath,
|
||||||
|
tool,
|
||||||
|
qaRun: false,
|
||||||
|
}
|
||||||
|
appendFileSync(qaQueueFile, JSON.stringify(entry) + '\n')
|
||||||
|
|
||||||
|
// Count pending files
|
||||||
|
if (existsSync(qaQueueFile)) {
|
||||||
|
try {
|
||||||
|
const lines = readFileSync(qaQueueFile, 'utf-8').split('\n').filter(Boolean)
|
||||||
|
const pending = lines.filter(l => {
|
||||||
|
try {
|
||||||
|
return !JSON.parse(l).qaRun
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}).length
|
||||||
|
|
||||||
|
if (pending >= 5) {
|
||||||
|
console.error(`\n📊 QA REMINDER: ${pending} files modified without QA scoring.`)
|
||||||
|
console.error(` Run: qa_score_file on modified files (target: 85+)\n`)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore read errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
189
.claude/hooks/track-tool-calls.ts
Executable file
189
.claude/hooks/track-tool-calls.ts
Executable file
@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* @file track-tool-calls.ts
|
||||||
|
* @description Tracks tool calls for meta-cognition enforcement
|
||||||
|
*
|
||||||
|
* Runs on every tool call to detect:
|
||||||
|
* - Search loops (same pattern 3+)
|
||||||
|
* - Error loops (same error 3+)
|
||||||
|
* - Context thrashing (many files, no completion)
|
||||||
|
*
|
||||||
|
* Claude Code passes input via STDIN as JSON.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
interface HookInput {
|
||||||
|
tool_name?: string
|
||||||
|
tool_input?: Record<string, unknown>
|
||||||
|
cwd?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCall {
|
||||||
|
timestamp: string
|
||||||
|
tool: string
|
||||||
|
input: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetaState {
|
||||||
|
sessionStart: string
|
||||||
|
toolCalls: number
|
||||||
|
searchPatterns: Record<string, number>
|
||||||
|
errorPatterns: Record<string, number>
|
||||||
|
filesAccessed: string[]
|
||||||
|
lastCheckpoint: number
|
||||||
|
warnings: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectRoot(): string {
|
||||||
|
return process.env.CLAUDE_PROJECT_DIR || process.cwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadState(stateFile: string): MetaState {
|
||||||
|
if (existsSync(stateFile)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||||
|
} catch {
|
||||||
|
// Corrupted, start fresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sessionStart: new Date().toISOString(),
|
||||||
|
toolCalls: 0,
|
||||||
|
searchPatterns: {},
|
||||||
|
errorPatterns: {},
|
||||||
|
filesAccessed: [],
|
||||||
|
lastCheckpoint: 0,
|
||||||
|
warnings: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState(stateFile: string, state: MetaState): void {
|
||||||
|
writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForLoops(state: MetaState): string[] {
|
||||||
|
const warnings: string[] = []
|
||||||
|
|
||||||
|
// Check search loops (3+ same pattern)
|
||||||
|
for (const [pattern, count] of Object.entries(state.searchPatterns)) {
|
||||||
|
if (count >= 3) {
|
||||||
|
warnings.push(`SEARCH_LOOP: "${pattern}" searched ${count} times. STOP and use systematic exploration.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check error loops
|
||||||
|
for (const [error, count] of Object.entries(state.errorPatterns)) {
|
||||||
|
if (count >= 3) {
|
||||||
|
warnings.push(`ERROR_LOOP: "${error}" occurred ${count} times. STOP and diagnose root cause.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check context thrashing (10+ files without completing work)
|
||||||
|
if (state.filesAccessed.length > 10 && state.toolCalls - state.lastCheckpoint > 20) {
|
||||||
|
warnings.push(`CONTEXT_THRASH: ${state.filesAccessed.length} files accessed. Focus on completing one task.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdin(): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let data = ''
|
||||||
|
process.stdin.setEncoding('utf8')
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
data += chunk
|
||||||
|
}
|
||||||
|
})
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
// Timeout after 1 second if no input
|
||||||
|
setTimeout(() => resolve(data), 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const projectRoot = getProjectRoot()
|
||||||
|
const logsDir = join(projectRoot, '.claude/logs')
|
||||||
|
const trackingFile = join(logsDir, 'tool-tracking.jsonl')
|
||||||
|
const stateFile = join(logsDir, 'meta-state.json')
|
||||||
|
|
||||||
|
// Ensure logs directory exists
|
||||||
|
mkdirSync(logsDir, { recursive: true })
|
||||||
|
|
||||||
|
// Parse input from Claude Code hook (via stdin)
|
||||||
|
let input: HookInput = {}
|
||||||
|
try {
|
||||||
|
const stdinData = await readStdin()
|
||||||
|
if (stdinData.trim()) {
|
||||||
|
input = JSON.parse(stdinData)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid JSON, continue with empty input
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = input.tool_name || 'unknown'
|
||||||
|
const toolInput = input.tool_input || {}
|
||||||
|
|
||||||
|
const state = loadState(stateFile)
|
||||||
|
state.toolCalls++
|
||||||
|
|
||||||
|
// Track the call
|
||||||
|
const call: ToolCall = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
tool,
|
||||||
|
input: toolInput,
|
||||||
|
}
|
||||||
|
appendFileSync(trackingFile, JSON.stringify(call) + '\n')
|
||||||
|
|
||||||
|
// Save pending call start time for PostToolUse duration calculation
|
||||||
|
const pendingFile = join(logsDir, '.pending-call.json')
|
||||||
|
writeFileSync(pendingFile, JSON.stringify({
|
||||||
|
tool,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
timestamp: call.timestamp,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Track patterns based on tool type
|
||||||
|
if (tool === 'Grep' || tool === 'Glob') {
|
||||||
|
const pattern = String(toolInput.pattern || '')
|
||||||
|
if (pattern) {
|
||||||
|
state.searchPatterns[pattern] = (state.searchPatterns[pattern] || 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool === 'Read' || tool === 'Edit' || tool === 'Write') {
|
||||||
|
const filePath = String(toolInput.file_path || '')
|
||||||
|
if (filePath && !state.filesAccessed.includes(filePath)) {
|
||||||
|
state.filesAccessed.push(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for meta-cognition triggers
|
||||||
|
const warnings = checkForLoops(state)
|
||||||
|
|
||||||
|
// Checkpoint check (every 10 actions)
|
||||||
|
if (state.toolCalls - state.lastCheckpoint >= 10) {
|
||||||
|
state.lastCheckpoint = state.toolCalls
|
||||||
|
warnings.push(`CHECKPOINT: ${state.toolCalls} tool calls. Review progress and flow state.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output warnings to stderr (will be shown to Claude)
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
state.warnings.push(...warnings)
|
||||||
|
console.error('\n⚠️ META-COGNITION ALERTS:')
|
||||||
|
warnings.forEach(w => console.error(` • ${w}`))
|
||||||
|
console.error('')
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(stateFile, state)
|
||||||
|
|
||||||
|
// Exit 0 to allow tool to proceed
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
245
.claude/hooks/track-tool-results.ts
Executable file
245
.claude/hooks/track-tool-results.ts
Executable file
@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* @file track-tool-results.ts
|
||||||
|
* @description PostToolUse hook that captures tool results, timing, and summaries
|
||||||
|
* @layer Hooks
|
||||||
|
*
|
||||||
|
* Runs after every tool call to record:
|
||||||
|
* - Duration (from PreToolUse start time)
|
||||||
|
* - Result summary (truncated, not full content)
|
||||||
|
* - Success/failure status
|
||||||
|
*
|
||||||
|
* Writes enriched entries to .claude/logs/tool-results.jsonl
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { appendFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'fs'
|
||||||
|
import { join, basename } from 'path'
|
||||||
|
|
||||||
|
interface PostHookInput {
|
||||||
|
session_id?: string
|
||||||
|
tool_name?: string
|
||||||
|
tool_input?: Record<string, unknown>
|
||||||
|
tool_output?: string
|
||||||
|
tool_result?: string
|
||||||
|
response?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnrichedToolResult {
|
||||||
|
timestamp: string
|
||||||
|
tool: string
|
||||||
|
durationMs: number
|
||||||
|
summary: string
|
||||||
|
status: 'success' | 'error'
|
||||||
|
meta: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectRoot(): string {
|
||||||
|
return process.env.CLAUDE_PROJECT_DIR || process.cwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarize tool output without storing full content.
|
||||||
|
* Extracts key metrics (line counts, file names, exit codes) not raw data.
|
||||||
|
*/
|
||||||
|
function summarizeOutput(tool: string, input: Record<string, unknown>, output: string): {
|
||||||
|
summary: string
|
||||||
|
status: 'success' | 'error'
|
||||||
|
meta: Record<string, unknown>
|
||||||
|
} {
|
||||||
|
const meta: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
if (!output || output.length === 0) {
|
||||||
|
return { summary: 'empty output', status: 'success', meta }
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = output.split('\n')
|
||||||
|
const lineCount = lines.length
|
||||||
|
const charCount = output.length
|
||||||
|
|
||||||
|
// Detect errors
|
||||||
|
const isError = /error|failed|not found|permission denied|ENOENT|EACCES/i.test(output.slice(0, 500))
|
||||||
|
|
||||||
|
switch (tool) {
|
||||||
|
case 'Read': {
|
||||||
|
const filePath = input.file_path as string || ''
|
||||||
|
const fileName = basename(filePath)
|
||||||
|
meta.file = fileName
|
||||||
|
meta.lines = lineCount
|
||||||
|
meta.chars = charCount
|
||||||
|
return {
|
||||||
|
summary: `${lineCount} lines from ${fileName} (${Math.round(charCount / 1024)}KB)`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Write': {
|
||||||
|
const filePath = input.file_path as string || ''
|
||||||
|
const fileName = basename(filePath)
|
||||||
|
const contentLines = (input.content as string || '').split('\n').length
|
||||||
|
meta.file = fileName
|
||||||
|
meta.lines = contentLines
|
||||||
|
return {
|
||||||
|
summary: `wrote ${contentLines} lines to ${fileName}`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Edit': {
|
||||||
|
const filePath = input.file_path as string || ''
|
||||||
|
const fileName = basename(filePath)
|
||||||
|
meta.file = fileName
|
||||||
|
meta.replaceAll = input.replace_all || false
|
||||||
|
return {
|
||||||
|
summary: `edited ${fileName}`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Bash': {
|
||||||
|
const cmd = (input.command as string || '').split('\n')[0].slice(0, 100)
|
||||||
|
meta.command = cmd
|
||||||
|
meta.outputLines = lineCount
|
||||||
|
// First meaningful line of output
|
||||||
|
const firstLine = lines.find(l => l.trim().length > 0) || ''
|
||||||
|
return {
|
||||||
|
summary: `${cmd} → ${lineCount} lines${isError ? ' (ERROR)' : ''}`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta: { ...meta, preview: firstLine.slice(0, 120) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Grep': {
|
||||||
|
// Count matches
|
||||||
|
const matchCount = lines.filter(l => l.trim().length > 0).length
|
||||||
|
meta.pattern = input.pattern
|
||||||
|
meta.matches = matchCount
|
||||||
|
return {
|
||||||
|
summary: `${matchCount} matches for "${(input.pattern as string || '').slice(0, 40)}"`,
|
||||||
|
status: 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Glob': {
|
||||||
|
const matchCount = lines.filter(l => l.trim().length > 0).length
|
||||||
|
meta.pattern = input.pattern
|
||||||
|
meta.matches = matchCount
|
||||||
|
return {
|
||||||
|
summary: `${matchCount} files matching "${(input.pattern as string || '').slice(0, 40)}"`,
|
||||||
|
status: 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Task': {
|
||||||
|
meta.agentType = input.subagent_type
|
||||||
|
meta.outputChars = charCount
|
||||||
|
return {
|
||||||
|
summary: `${input.subagent_type || 'agent'} task (${Math.round(charCount / 1024)}KB output)`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'WebSearch': {
|
||||||
|
meta.query = input.query
|
||||||
|
return {
|
||||||
|
summary: `search: "${(input.query as string || '').slice(0, 60)}"`,
|
||||||
|
status: 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'WebFetch': {
|
||||||
|
meta.url = input.url
|
||||||
|
return {
|
||||||
|
summary: `fetch: ${(input.url as string || '').slice(0, 80)}`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
summary: `${tool}: ${charCount} chars output`,
|
||||||
|
status: isError ? 'error' : 'success',
|
||||||
|
meta: { outputChars: charCount },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdin(): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let data = ''
|
||||||
|
process.stdin.setEncoding('utf8')
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
data += chunk
|
||||||
|
}
|
||||||
|
})
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
setTimeout(() => resolve(data), 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const projectRoot = getProjectRoot()
|
||||||
|
const logsDir = join(projectRoot, '.claude/logs')
|
||||||
|
const resultsFile = join(logsDir, 'tool-results.jsonl')
|
||||||
|
const pendingFile = join(logsDir, '.pending-call.json')
|
||||||
|
|
||||||
|
mkdirSync(logsDir, { recursive: true })
|
||||||
|
|
||||||
|
// Parse PostToolUse input
|
||||||
|
let input: PostHookInput = {}
|
||||||
|
try {
|
||||||
|
const stdinData = await readStdin()
|
||||||
|
if (stdinData.trim()) {
|
||||||
|
input = JSON.parse(stdinData)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = input.tool_name || 'unknown'
|
||||||
|
const toolInput = input.tool_input || {}
|
||||||
|
const toolOutput = input.tool_output || input.tool_result || input.response || ''
|
||||||
|
|
||||||
|
// Calculate duration from pending call
|
||||||
|
let durationMs = 0
|
||||||
|
if (existsSync(pendingFile)) {
|
||||||
|
try {
|
||||||
|
const pending = JSON.parse(readFileSync(pendingFile, 'utf-8'))
|
||||||
|
durationMs = Date.now() - (pending.startedAt || Date.now())
|
||||||
|
unlinkSync(pendingFile)
|
||||||
|
} catch {
|
||||||
|
// Missing or corrupted pending file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summarize the result
|
||||||
|
const { summary, status, meta } = summarizeOutput(tool, toolInput, toolOutput)
|
||||||
|
|
||||||
|
// Write enriched result
|
||||||
|
const entry: EnrichedToolResult = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
tool,
|
||||||
|
durationMs,
|
||||||
|
summary,
|
||||||
|
status,
|
||||||
|
meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
appendFileSync(resultsFile, JSON.stringify(entry) + '\n')
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
396
.claude/hooks/unicorn-boot.ts
Executable file
396
.claude/hooks/unicorn-boot.ts
Executable file
@ -0,0 +1,396 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* @file unicorn-boot.ts
|
||||||
|
* @description SessionStart hook for UnicornDev framework initialization
|
||||||
|
* @layer Infrastructure
|
||||||
|
*
|
||||||
|
* Provides:
|
||||||
|
* - Global repo health overview (for awareness)
|
||||||
|
* - Prompt scoping workflow for per-task analysis
|
||||||
|
* - Quality thresholds that apply to SCOPED analysis, not global counts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync } from 'fs'
|
||||||
|
import { join, basename, extname } from 'path'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface Settings {
|
||||||
|
version?: string
|
||||||
|
techLevel?: string
|
||||||
|
proactivityLevel?: string
|
||||||
|
interactionLevel?: string
|
||||||
|
autonomy?: {
|
||||||
|
mode?: string
|
||||||
|
selfRecover?: boolean
|
||||||
|
continueUntilDone?: boolean
|
||||||
|
askUser?: string
|
||||||
|
}
|
||||||
|
quality?: {
|
||||||
|
minQaScore?: number
|
||||||
|
requireSpecs?: boolean
|
||||||
|
requireTests?: boolean
|
||||||
|
thresholds?: QualityThresholds
|
||||||
|
}
|
||||||
|
metaCognition?: { enabled?: boolean }
|
||||||
|
learning?: { enabled?: boolean }
|
||||||
|
execution?: { checkpointInterval?: number }
|
||||||
|
autoWorkflow?: {
|
||||||
|
autoCommit?: boolean
|
||||||
|
commitStyle?: string
|
||||||
|
}
|
||||||
|
workflow?: {
|
||||||
|
type?: string
|
||||||
|
gitFlow?: string
|
||||||
|
integrationBranch?: string
|
||||||
|
branchPrefix?: string
|
||||||
|
commitStrategy?: string
|
||||||
|
commitDirectly?: boolean
|
||||||
|
pushAfterCommit?: boolean
|
||||||
|
createPR?: boolean
|
||||||
|
qaRequired?: boolean
|
||||||
|
specsRequired?: boolean
|
||||||
|
testRequired?: boolean
|
||||||
|
commitPerTask?: boolean
|
||||||
|
qaLoop?: { enabled?: boolean; testLocally?: boolean }
|
||||||
|
recommendations?: Record<string, boolean>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QualityThresholds {
|
||||||
|
// Scoped thresholds (applied to prompt scope, not global)
|
||||||
|
scopedSpecCoverage: number // % of scoped features with specs (default: 80)
|
||||||
|
scopedImplComplete: number // % of spec criteria implemented (default: 70)
|
||||||
|
scopedTestCoverage: number // % test coverage for scoped files (default: 60)
|
||||||
|
scopedQaScore: number // Min QA score for scoped files (default: 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_THRESHOLDS: QualityThresholds = {
|
||||||
|
scopedSpecCoverage: 80, // 80% of touched features should have specs
|
||||||
|
scopedImplComplete: 70, // 70% of spec criteria should be implemented
|
||||||
|
scopedTestCoverage: 60, // 60% test coverage for modified files
|
||||||
|
scopedQaScore: 80 // QA score 80+ for modified files
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RepoOverview {
|
||||||
|
specCount: number
|
||||||
|
codeFileCount: number
|
||||||
|
testFileCount: number
|
||||||
|
structure: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Utilities
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function getProjectRoot(): string {
|
||||||
|
return process.env.CLAUDE_PROJECT_DIR || process.cwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeReadJson<T>(path: string, defaultValue: T): T {
|
||||||
|
try {
|
||||||
|
if (existsSync(path)) {
|
||||||
|
return JSON.parse(readFileSync(path, 'utf-8'))
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGitInfo(): { branch: string; status: string; flow: string } {
|
||||||
|
try {
|
||||||
|
const branch = execSync('git branch --show-current 2>/dev/null', { encoding: 'utf-8' }).trim() || 'unknown'
|
||||||
|
const statusOutput = execSync('git status --short 2>/dev/null', { encoding: 'utf-8' }).trim()
|
||||||
|
const lines = statusOutput.split('\n').filter(Boolean)
|
||||||
|
const status = lines.length > 0 ? `${lines.length} changed` : 'clean'
|
||||||
|
|
||||||
|
const branchList = execSync('git branch -a 2>/dev/null', { encoding: 'utf-8' })
|
||||||
|
let flow = 'trunk'
|
||||||
|
if (/\bdevelop\b|\bdev\b/.test(branchList)) flow = 'gitflow'
|
||||||
|
else if (/feature\/|feat\/|fix\//.test(branchList)) flow = 'github-flow'
|
||||||
|
|
||||||
|
return { branch, status, flow }
|
||||||
|
} catch {
|
||||||
|
return { branch: 'unknown', status: 'unknown', flow: 'trunk' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFiles(dir: string, extensions: string[], maxDepth = 4): string[] {
|
||||||
|
const results: string[] = []
|
||||||
|
const excludeDirs = ['node_modules', 'dist', '.next', 'coverage', '__tests__', '.git']
|
||||||
|
|
||||||
|
function walk(currentDir: string, depth: number): void {
|
||||||
|
if (depth > maxDepth) return
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(currentDir, { withFileTypes: true })
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = join(currentDir, entry.name)
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (!excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) {
|
||||||
|
walk(fullPath, depth + 1)
|
||||||
|
}
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
if (extensions.includes(extname(entry.name))) {
|
||||||
|
results.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(dir)) walk(dir, 0)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Repo Overview (Quick Global Stats)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function getRepoOverview(projectRoot: string): RepoOverview {
|
||||||
|
let specCount = 0
|
||||||
|
let codeFileCount = 0
|
||||||
|
let testFileCount = 0
|
||||||
|
let structure = 'unknown'
|
||||||
|
|
||||||
|
// Count specs
|
||||||
|
for (const dir of ['specs', 'docs'].map(d => join(projectRoot, d))) {
|
||||||
|
specCount += findFiles(dir, ['.md'], 3).length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count code and tests
|
||||||
|
for (const dir of ['src', 'app', 'packages', 'lib'].map(d => join(projectRoot, d))) {
|
||||||
|
const files = findFiles(dir, ['.ts', '.tsx'], 4)
|
||||||
|
for (const f of files) {
|
||||||
|
if (f.includes('.test.') || f.includes('.spec.')) {
|
||||||
|
testFileCount++
|
||||||
|
} else {
|
||||||
|
codeFileCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect structure
|
||||||
|
const pkgPath = join(projectRoot, 'package.json')
|
||||||
|
if (existsSync(pkgPath)) {
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
||||||
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
||||||
|
if (deps['next']) structure = existsSync(join(projectRoot, 'app')) ? 'nextjs-app' : 'nextjs-pages'
|
||||||
|
else if (deps['react']) structure = 'react'
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (existsSync(join(projectRoot, 'packages')) || existsSync(join(projectRoot, 'apps'))) {
|
||||||
|
structure = 'monorepo'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { specCount, codeFileCount, testFileCount, structure }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Logging
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function logSessionStart(logsDir: string, branch: string): void {
|
||||||
|
const logFile = join(logsDir, `session-${new Date().toISOString().split('T')[0]}.jsonl`)
|
||||||
|
appendFileSync(logFile, JSON.stringify({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
event: 'session_start',
|
||||||
|
branch,
|
||||||
|
type: 'SessionStart_hook'
|
||||||
|
}) + '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeMetaState(logsDir: string): void {
|
||||||
|
writeFileSync(join(logsDir, 'meta-state.json'), JSON.stringify({
|
||||||
|
sessionStart: new Date().toISOString(),
|
||||||
|
toolCalls: 0,
|
||||||
|
searchPatterns: {},
|
||||||
|
errorPatterns: {},
|
||||||
|
filesAccessed: [],
|
||||||
|
lastCheckpoint: 0,
|
||||||
|
warnings: [],
|
||||||
|
bootedVia: 'SessionStart_hook'
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Main
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function main(): void {
|
||||||
|
const projectRoot = getProjectRoot()
|
||||||
|
const logsDir = join(projectRoot, '.claude/logs')
|
||||||
|
mkdirSync(logsDir, { recursive: true })
|
||||||
|
|
||||||
|
const settings = safeReadJson<Settings>(join(projectRoot, '.claude/settings.json'), {})
|
||||||
|
const knowledge = safeReadJson<{ entries?: Record<string, unknown> }>(
|
||||||
|
join(projectRoot, '.claude/knowledge/index.json'),
|
||||||
|
{ entries: {} }
|
||||||
|
)
|
||||||
|
const knowledgeCount = Object.keys(knowledge.entries || {}).length
|
||||||
|
|
||||||
|
const git = getGitInfo()
|
||||||
|
const repo = getRepoOverview(projectRoot)
|
||||||
|
|
||||||
|
logSessionStart(logsDir, git.branch)
|
||||||
|
initializeMetaState(logsDir)
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const techLevel = settings.techLevel || 'relaxed'
|
||||||
|
const techScales: Record<string, number> = { hardcore: 5, strict: 4, relaxed: 3, lenient: 2, sloppy: 1 }
|
||||||
|
const techScale = techScales[techLevel] || 3
|
||||||
|
const proactivity = settings.proactivityLevel || 'autonomous'
|
||||||
|
const proactivityScales: Record<string, number> = { autonomous: 5, proactive: 4, careful: 3, cautious: 2, paranoid: 1 }
|
||||||
|
const proactivityScale = proactivityScales[proactivity] || 5
|
||||||
|
const minQaScore = settings.quality?.minQaScore || 85
|
||||||
|
const requireSpecs = settings.quality?.requireSpecs !== false
|
||||||
|
const workflowType = settings.workflow?.type || 'spec-first'
|
||||||
|
const commitPerTask = settings.workflow?.commitPerTask !== false
|
||||||
|
const autoCommit = settings.autoWorkflow?.autoCommit !== false
|
||||||
|
const commitStyle = settings.autoWorkflow?.commitStyle || 'conventional'
|
||||||
|
const autonomyMode = settings.autonomy?.mode || 'full'
|
||||||
|
const selfRecover = settings.autonomy?.selfRecover !== false
|
||||||
|
const checkpoint = settings.execution?.checkpointInterval || 10
|
||||||
|
|
||||||
|
// Git workflow settings
|
||||||
|
const gitFlow = settings.workflow?.gitFlow || git.flow
|
||||||
|
const integrationBranch = settings.workflow?.integrationBranch || (gitFlow === 'gitflow' ? 'develop' : 'main')
|
||||||
|
const branchPrefix = settings.workflow?.branchPrefix || 'feat/'
|
||||||
|
const commitStrategy = settings.workflow?.commitStrategy || commitStyle
|
||||||
|
const commitDirectly = settings.workflow?.commitDirectly ?? (gitFlow === 'trunk')
|
||||||
|
const pushAfterCommit = settings.workflow?.pushAfterCommit ?? settings.autoWorkflow?.pushAfterCommit ?? false
|
||||||
|
const createPR = settings.workflow?.createPR ?? false
|
||||||
|
const qaRequired = settings.workflow?.qaRequired !== false
|
||||||
|
const specsRequired = settings.workflow?.specsRequired !== false
|
||||||
|
const testRequired = settings.workflow?.testRequired ?? false
|
||||||
|
const recommendations = settings.workflow?.recommendations ?? {}
|
||||||
|
|
||||||
|
// Build lifecycle string
|
||||||
|
const lifecycle = [
|
||||||
|
specsRequired ? 'spec' : null,
|
||||||
|
!commitDirectly ? 'branch' : null,
|
||||||
|
'implement',
|
||||||
|
qaRequired ? 'QA' : null,
|
||||||
|
'commit',
|
||||||
|
pushAfterCommit ? 'push' : null,
|
||||||
|
createPR ? 'PR' : null,
|
||||||
|
].filter(Boolean).join(' → ')
|
||||||
|
|
||||||
|
// Build recommendations string
|
||||||
|
const recsActive = Object.entries(recommendations).filter(([, v]) => v).map(([k]) => {
|
||||||
|
const labels: Record<string, string> = { featureBranch: 'feature branches', fullQA: 'full QA', specFirst: 'spec-first', codeReview: 'code review' }
|
||||||
|
return labels[k] || k
|
||||||
|
})
|
||||||
|
|
||||||
|
// Thresholds (scoped, not global)
|
||||||
|
const thresholds: QualityThresholds = { ...DEFAULT_THRESHOLDS, ...settings.quality?.thresholds }
|
||||||
|
|
||||||
|
const context = `🦄 UnicornDev Boot
|
||||||
|
|
||||||
|
## CONFIG
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| tech | ${techLevel} (${techScale}/5) |
|
||||||
|
| proactive | ${proactivity} (${proactivityScale}/5) |
|
||||||
|
| qa | ≥${minQaScore} (${Math.round(minQaScore/20)}/5) |
|
||||||
|
| workflow | ${workflowType} |
|
||||||
|
|
||||||
|
**Autonomy:** mode=${autonomyMode} self-recover=${selfRecover ? 'on' : 'off'}
|
||||||
|
**State:** branch=\`${git.branch}\` git=${git.status} flow=${git.flow}
|
||||||
|
**Commits:** ${autoCommit ? commitStyle : 'manual'}${commitPerTask ? ' (per-task)' : ''}
|
||||||
|
|
||||||
|
## REPO OVERVIEW
|
||||||
|
- Structure: ${repo.structure}
|
||||||
|
- Specs: ${repo.specCount} | Code: ${repo.codeFileCount} | Tests: ${repo.testFileCount}
|
||||||
|
- Knowledge entries: ${knowledgeCount}
|
||||||
|
|
||||||
|
## GIT WORKFLOW
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| flow | ${gitFlow} |
|
||||||
|
| integration | \`${integrationBranch}\` |
|
||||||
|
| branch prefix | \`${branchPrefix}\` |
|
||||||
|
| commit | ${commitDirectly ? 'direct to branch' : 'feature branch required'} |
|
||||||
|
| strategy | ${commitStrategy} |
|
||||||
|
| push | ${pushAfterCommit ? 'auto after commit' : 'manual'} |
|
||||||
|
| PR | ${createPR ? 'auto-create to ' + integrationBranch : 'manual'} |
|
||||||
|
| QA gate | ${qaRequired ? 'required' : 'optional'} |
|
||||||
|
|
||||||
|
**Task Lifecycle:** ${lifecycle}${recsActive.length > 0 ? `\n**Recommendations:** ${recsActive.join(', ')}` : ''}
|
||||||
|
|
||||||
|
## QUALITY THRESHOLDS (Applied to Prompt Scope)
|
||||||
|
| Metric | Threshold | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| Spec Coverage | ≥${thresholds.scopedSpecCoverage}% | Features in scope must have specs |
|
||||||
|
| Impl Complete | ≥${thresholds.scopedImplComplete}% | Spec criteria implemented |
|
||||||
|
| Test Coverage | ≥${thresholds.scopedTestCoverage}% | Tests for modified files |
|
||||||
|
| QA Score | ≥${thresholds.scopedQaScore} | Quality score for scope |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WORKFLOW: Prompt Scoping Protocol
|
||||||
|
|
||||||
|
### PHASE 1: SCOPE THE PROMPT
|
||||||
|
Before any code, determine what the prompt touches:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
PROMPT SCOPE ANALYSIS
|
||||||
|
├─ Features: [list features/modules involved]
|
||||||
|
├─ Files: [list files likely to be modified]
|
||||||
|
├─ Dependencies: [upstream/downstream impacts]
|
||||||
|
└─ Boundaries: [what's IN vs OUT of scope]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### PHASE 2: SCOPED QUALITY CHECK
|
||||||
|
For the scoped features/files, assess:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
SCOPED ANALYSIS
|
||||||
|
├─ Spec Coverage: X% (${thresholds.scopedSpecCoverage}% required)
|
||||||
|
│ └─ [list features WITH specs vs WITHOUT]
|
||||||
|
├─ Implementation Differential:
|
||||||
|
│ ├─ ✓ DONE: [criteria already implemented]
|
||||||
|
│ ├─ ◐ PARTIAL: [criteria partially done]
|
||||||
|
│ └─ ✗ REMAINING: [criteria to implement]
|
||||||
|
├─ Test Coverage: X% (${thresholds.scopedTestCoverage}% required)
|
||||||
|
│ └─ [files with tests vs without]
|
||||||
|
└─ QA Scores: [scores for files in scope]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### PHASE 3: RECOMMENDATIONS (Scoped)
|
||||||
|
Based on scoped analysis, determine blockers:
|
||||||
|
|
||||||
|
**P0 BLOCKER** (must fix first):
|
||||||
|
- Scoped spec coverage < ${thresholds.scopedSpecCoverage}% → Create specs for uncovered features
|
||||||
|
- Critical dependencies missing specs → Spec those first
|
||||||
|
|
||||||
|
**P1 IMPORTANT** (address during task):
|
||||||
|
- Implementation differential shows gaps → Close gaps in order
|
||||||
|
- Test coverage < ${thresholds.scopedTestCoverage}% → Add tests for modified files
|
||||||
|
|
||||||
|
**P2 ENHANCEMENT** (note for later):
|
||||||
|
- QA scores below ${thresholds.scopedQaScore} → Improve after core work done
|
||||||
|
|
||||||
|
### PHASE 4: EXECUTE (${workflowType})
|
||||||
|
${requireSpecs ? '1. **SPEC** - Create/update specs for uncovered scope' : '1. Skip spec'}
|
||||||
|
2. **IMPLEMENT** - Work through implementation differential in order
|
||||||
|
3. **TEST** - Add/update tests for modified files
|
||||||
|
4. **COMMIT** - ${commitPerTask ? 'Semantic commit per logical unit' : 'Batch commits'}
|
||||||
|
5. **QA** - Verify scoped files meet qa≥${minQaScore}
|
||||||
|
|
||||||
|
### PHASE 5: VERIFY SCOPE
|
||||||
|
Before completing:
|
||||||
|
- [ ] All scoped features have specs (≥${thresholds.scopedSpecCoverage}%)
|
||||||
|
- [ ] Implementation differential closed (≥${thresholds.scopedImplComplete}%)
|
||||||
|
- [ ] Modified files have tests (≥${thresholds.scopedTestCoverage}%)
|
||||||
|
- [ ] QA scores meet threshold (≥${thresholds.scopedQaScore})
|
||||||
|
|
||||||
|
**MODE:** ${autonomyMode === 'full' ? 'AUTONOMOUS - Work until done, self-recover' : 'SUPERVISED'}
|
||||||
|
**Checkpoints:** Every ${checkpoint} actions, verify scope progress`
|
||||||
|
|
||||||
|
console.log(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
0
.claude/knowledge/experimental/.gitkeep
Normal file
0
.claude/knowledge/experimental/.gitkeep
Normal file
0
.claude/knowledge/frameworks/.gitkeep
Normal file
0
.claude/knowledge/frameworks/.gitkeep
Normal file
5
.claude/knowledge/index.json
Normal file
5
.claude/knowledge/index.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lastUpdated": "2026-02-27T02:04:42.813Z",
|
||||||
|
"entries": []
|
||||||
|
}
|
||||||
0
.claude/knowledge/integrations/.gitkeep
Normal file
0
.claude/knowledge/integrations/.gitkeep
Normal file
0
.claude/knowledge/libraries/.gitkeep
Normal file
0
.claude/knowledge/libraries/.gitkeep
Normal file
0
.claude/knowledge/patterns/.gitkeep
Normal file
0
.claude/knowledge/patterns/.gitkeep
Normal file
0
.claude/knowledge/techniques/.gitkeep
Normal file
0
.claude/knowledge/techniques/.gitkeep
Normal file
261
.claude/settings.json
Normal file
261
.claude/settings.json
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"version": "1.2.2",
|
||||||
|
"techLevel": "strict",
|
||||||
|
"interactionLevel": "yolo",
|
||||||
|
"proactivityLevel": "autonomous",
|
||||||
|
"autonomy": {
|
||||||
|
"mode": "full",
|
||||||
|
"stopOnError": false,
|
||||||
|
"stopOnUncertainty": false,
|
||||||
|
"selfRecover": true,
|
||||||
|
"continueUntilDone": true,
|
||||||
|
"askUser": "never"
|
||||||
|
},
|
||||||
|
"autoWorkflow": {
|
||||||
|
"enabled": true,
|
||||||
|
"expandPrompts": true,
|
||||||
|
"autoCommit": true,
|
||||||
|
"commitStyle": "conventional",
|
||||||
|
"commitFrequency": "logical-units",
|
||||||
|
"pushAfterCommit": false,
|
||||||
|
"createBranch": false
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"bootOnSessionStart": true,
|
||||||
|
"checkpointInterval": 10,
|
||||||
|
"maxNonProgressActions": 5,
|
||||||
|
"maxRetries": 3,
|
||||||
|
"maxRecoveryAttempts": 3,
|
||||||
|
"logLevel": "normal",
|
||||||
|
"preActionCheck": true,
|
||||||
|
"postTaskReview": true
|
||||||
|
},
|
||||||
|
"learning": {
|
||||||
|
"enabled": true,
|
||||||
|
"autoLearn": true,
|
||||||
|
"askBeforeCreating": false,
|
||||||
|
"notifyOnNewKnowledge": true,
|
||||||
|
"sources": {
|
||||||
|
"dependencies": true,
|
||||||
|
"imports": true,
|
||||||
|
"userRequests": true,
|
||||||
|
"webResearch": true
|
||||||
|
},
|
||||||
|
"ignore": [],
|
||||||
|
"categories": {
|
||||||
|
"frameworks": true,
|
||||||
|
"libraries": true,
|
||||||
|
"languages": true,
|
||||||
|
"patterns": true,
|
||||||
|
"techniques": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metaCognition": {
|
||||||
|
"enabled": true,
|
||||||
|
"autoFixBlockers": true,
|
||||||
|
"autoOptimize": false,
|
||||||
|
"thresholds": {
|
||||||
|
"retryWarning": 2,
|
||||||
|
"retryError": 4,
|
||||||
|
"searchLoopWarning": 3,
|
||||||
|
"errorLoopCritical": 3,
|
||||||
|
"noProgressActions": 5,
|
||||||
|
"slowdownPercent": 50,
|
||||||
|
"contextSwitchLimit": 3,
|
||||||
|
"uncertaintyKeywords": [
|
||||||
|
"might",
|
||||||
|
"perhaps",
|
||||||
|
"not sure",
|
||||||
|
"maybe",
|
||||||
|
"possibly",
|
||||||
|
"I think",
|
||||||
|
"could be"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"flowIndicators": {
|
||||||
|
"positive": [
|
||||||
|
"completed",
|
||||||
|
"found",
|
||||||
|
"created",
|
||||||
|
"fixed",
|
||||||
|
"working",
|
||||||
|
"success",
|
||||||
|
"verified",
|
||||||
|
"passed"
|
||||||
|
],
|
||||||
|
"negative": [
|
||||||
|
"failed",
|
||||||
|
"error",
|
||||||
|
"not found",
|
||||||
|
"retry",
|
||||||
|
"stuck",
|
||||||
|
"confused",
|
||||||
|
"uncertain",
|
||||||
|
"regression"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"complexityAwareness": {
|
||||||
|
"reassessOnDifficulty": true,
|
||||||
|
"scaleUpWhenStruggling": true,
|
||||||
|
"detectComplexityMismatch": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"minQaScore": 85,
|
||||||
|
"requireSpecs": true,
|
||||||
|
"requireTests": false,
|
||||||
|
"thresholds": {
|
||||||
|
"scopedSpecCoverage": 80,
|
||||||
|
"scopedImplComplete": 70,
|
||||||
|
"scopedTestCoverage": 60,
|
||||||
|
"scopedQaScore": 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"type": "spec-first",
|
||||||
|
"gitFlow": "trunk",
|
||||||
|
"commitPerTask": true,
|
||||||
|
"qaLoop": {
|
||||||
|
"enabled": true,
|
||||||
|
"testLocally": true,
|
||||||
|
"verifySpec": true,
|
||||||
|
"diffReport": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"matcher": "startup",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/unicorn-boot.ts",
|
||||||
|
"timeout": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "resume",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/unicorn-boot.ts",
|
||||||
|
"timeout": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/track-tool-calls.ts",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Write|Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/qa-after-write.ts",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"complexityScaling": {
|
||||||
|
"enabled": true,
|
||||||
|
"assessBeforeTask": true,
|
||||||
|
"autoScaleMethodology": true,
|
||||||
|
"maxIncrementMinutes": 30,
|
||||||
|
"levels": {
|
||||||
|
"trivial": {
|
||||||
|
"maxScore": 1.5,
|
||||||
|
"checkpoint": "none",
|
||||||
|
"verification": [
|
||||||
|
"L0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"simple": {
|
||||||
|
"maxScore": 2.5,
|
||||||
|
"checkpoint": "soft",
|
||||||
|
"verification": [
|
||||||
|
"L0",
|
||||||
|
"L1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"moderate": {
|
||||||
|
"maxScore": 3.5,
|
||||||
|
"checkpoint": "hard",
|
||||||
|
"verification": [
|
||||||
|
"L0",
|
||||||
|
"L1",
|
||||||
|
"L2",
|
||||||
|
"L4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"complex": {
|
||||||
|
"maxScore": 4.5,
|
||||||
|
"checkpoint": "verified",
|
||||||
|
"verification": [
|
||||||
|
"L0",
|
||||||
|
"L1",
|
||||||
|
"L2",
|
||||||
|
"L3",
|
||||||
|
"L4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extreme": {
|
||||||
|
"maxScore": 5,
|
||||||
|
"checkpoint": "verified",
|
||||||
|
"verification": [
|
||||||
|
"L0",
|
||||||
|
"L1",
|
||||||
|
"L2",
|
||||||
|
"L3",
|
||||||
|
"L4"
|
||||||
|
],
|
||||||
|
"userCheckpoints": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"regressionProtection": {
|
||||||
|
"alwaysRunL4AfterPhase": true,
|
||||||
|
"revertOnMajorRegression": true,
|
||||||
|
"requireVerifiedCheckpoint": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"boot": {
|
||||||
|
"auto": true,
|
||||||
|
"trigger": "session_start"
|
||||||
|
},
|
||||||
|
"assess": {
|
||||||
|
"auto": true,
|
||||||
|
"trigger": "task_start"
|
||||||
|
},
|
||||||
|
"checkpoint": {
|
||||||
|
"auto": true,
|
||||||
|
"trigger": "interval",
|
||||||
|
"interval": 10
|
||||||
|
},
|
||||||
|
"verify": {
|
||||||
|
"auto": true,
|
||||||
|
"trigger": "after_change"
|
||||||
|
},
|
||||||
|
"reflect": {
|
||||||
|
"auto": true,
|
||||||
|
"trigger": "blocker_detected"
|
||||||
|
},
|
||||||
|
"learn": {
|
||||||
|
"auto": true,
|
||||||
|
"trigger": "unknown_tech_detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
.next/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
*.jsonl
|
||||||
|
.claude/logs/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
193
CLAUDE.md
Normal file
193
CLAUDE.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# webproxy
|
||||||
|
|
||||||
|
> Project context and instructions for Claude Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🦄 UnicornDev Framework
|
||||||
|
|
||||||
|
This project uses UnicornDev enforcement for consistent, high-quality development.
|
||||||
|
|
||||||
|
**Version:** 1.2.2
|
||||||
|
**Docs:** `.claude/RUNBOOK.md` (quick reference)
|
||||||
|
|
||||||
|
### Standard Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
RECEIVE → CLASSIFY → EXECUTE → VERIFY → RESPOND
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ │ CHECKPOINT ◄────┘ (every 10 actions)
|
||||||
|
│ │ │
|
||||||
|
│ ▼ ▼
|
||||||
|
│ [complexity] [if stuck]
|
||||||
|
│ SCALING REFLECT → IMPROVE
|
||||||
|
└─────────────────────────────────────► (loop)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Files (Read These)
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `.claude/RUNBOOK.md` | Quick reference for all behaviors |
|
||||||
|
| `.claude/settings.json` | Project-specific configuration |
|
||||||
|
| `.claude/knowledge/index.json` | What technologies are known |
|
||||||
|
| `.claude/logs/` | Session tracking and insights |
|
||||||
|
|
||||||
|
### Settings Summary
|
||||||
|
|
||||||
|
Settings in `.claude/settings.json` control behavior:
|
||||||
|
|
||||||
|
- **techLevel**: `hardcore` (5/5) / `strict` (4/5) / `relaxed` (3/5) / `lenient` (2/5) / `sloppy` (1/5)
|
||||||
|
- **proactivityLevel**: `autonomous` (5/5) / `proactive` (4/5) / `careful` (3/5) / `cautious` (2/5) / `paranoid` (1/5)
|
||||||
|
- **metaCognition.enabled**: Self-monitoring for loops and blockers
|
||||||
|
- **learning.enabled**: Auto-document new technologies
|
||||||
|
- **quality.minQaScore**: Minimum QA score (default: 85)
|
||||||
|
- **quality.thresholds**: Scoped quality thresholds (spec coverage, impl complete, test coverage, QA)
|
||||||
|
|
||||||
|
### Complexity Scaling
|
||||||
|
|
||||||
|
Before starting tasks, assess complexity (1-5):
|
||||||
|
- **SCOPE**: Single function → Multiple systems
|
||||||
|
- **UNCERTAINTY**: Known solution → Unprecedented
|
||||||
|
- **RISK**: Safe → Critical
|
||||||
|
|
||||||
|
| Score | Level | Methodology |
|
||||||
|
|-------|-------|-------------|
|
||||||
|
| 1.0-1.5 | TRIVIAL | Do → Verify → Done |
|
||||||
|
| 1.6-2.5 | SIMPLE | Plan → Do → Test |
|
||||||
|
| 2.6-3.5 | MODERATE | Tasks → [Do → Verify → Regression]* |
|
||||||
|
| 3.6+ | COMPLEX | Phases → Checkpoints → User approval |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ MANDATORY ENFORCEMENT (READ FIRST)
|
||||||
|
|
||||||
|
**This section is NON-NEGOTIABLE. Execute these checks AUTOMATICALLY.**
|
||||||
|
|
||||||
|
### 🔴 ON EVERY SESSION START (BOOT)
|
||||||
|
|
||||||
|
Before responding to ANY user message, you MUST:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. READ .claude/settings.json → Apply all settings
|
||||||
|
2. READ .claude/knowledge/index.json → Load known technologies
|
||||||
|
3. CHECK git status → Note current branch and state
|
||||||
|
4. LOG session start in .claude/logs/session-{date}.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
If any file is missing, CREATE it with defaults.
|
||||||
|
|
||||||
|
### 🔴 ON EVERY 10 TOOL CALLS (CHECKPOINT)
|
||||||
|
|
||||||
|
After every 10 tool calls, STOP and verify:
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Am I making progress? (If NO → Reflect)
|
||||||
|
□ Have I repeated any search 3+ times? (If YES → STOP, try different approach)
|
||||||
|
□ Have I hit the same error 3+ times? (If YES → STOP, diagnose root cause)
|
||||||
|
□ Am I working on too many files? (If 5+ open → Focus on ONE)
|
||||||
|
```
|
||||||
|
|
||||||
|
If ANY check fails: **STOP. Do not continue. Reflect and fix first.**
|
||||||
|
|
||||||
|
### 🔴 ON EVERY FILE WRITE/EDIT (QUALITY)
|
||||||
|
|
||||||
|
After writing or editing code files:
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Run qa_score_file on the file (target: 85+)
|
||||||
|
□ If score < 85 → Fix before proceeding
|
||||||
|
□ Log file change in .claude/logs/changes.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 ON UNKNOWN TECHNOLOGY (LEARN)
|
||||||
|
|
||||||
|
When encountering technology not in `.claude/knowledge/index.json`:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. STOP current task
|
||||||
|
2. Research the technology (docs, best practices)
|
||||||
|
3. CREATE .claude/knowledge/{category}/{name}.md
|
||||||
|
4. UPDATE .claude/knowledge/index.json
|
||||||
|
5. NOTIFY user: "📚 Learned: {name}"
|
||||||
|
6. RESUME task with new knowledge
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 ON TASK COMPLETION (VERIFY)
|
||||||
|
|
||||||
|
Before saying a task is done:
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Build passes? (pnpm build / cargo build)
|
||||||
|
□ Types check? (tsc --noEmit)
|
||||||
|
□ QA scores ≥ 85 on changed files?
|
||||||
|
□ No regressions? (existing tests pass)
|
||||||
|
□ Acceptance criteria met?
|
||||||
|
```
|
||||||
|
|
||||||
|
If ANY check fails: **Task is NOT done. Fix first.**
|
||||||
|
|
||||||
|
### 🔴 SEARCH LOOP RECOVERY
|
||||||
|
|
||||||
|
If you've searched for the same thing 3+ times:
|
||||||
|
|
||||||
|
```
|
||||||
|
STOP searching. Instead:
|
||||||
|
1. Read package.json to see what libraries exist
|
||||||
|
2. List the directory structure: ls -la
|
||||||
|
3. Read the entry point file
|
||||||
|
4. Follow imports systematically
|
||||||
|
5. If still stuck, ASK user for guidance
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 ERROR LOOP RECOVERY
|
||||||
|
|
||||||
|
If you've hit the same error 3+ times:
|
||||||
|
|
||||||
|
```
|
||||||
|
STOP retrying. Instead:
|
||||||
|
1. Log the error clearly
|
||||||
|
2. Ask: "What assumption am I making that's wrong?"
|
||||||
|
3. Try a DIFFERENT approach, not the same one again
|
||||||
|
4. If still stuck, ASK user for guidance
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 ON COMMIT (GIT HYGIENE)
|
||||||
|
|
||||||
|
When user asks to commit (especially "commit everything"):
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Check git status for untracked files
|
||||||
|
2. Identify generated files (logs, build output, cache, etc.)
|
||||||
|
3. Update .gitignore BEFORE staging if needed
|
||||||
|
4. Stage source files only
|
||||||
|
5. Commit with conventional commit message
|
||||||
|
6. Push if requested
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated files to always gitignore:**
|
||||||
|
- `*.log`, `*.jsonl` (logs)
|
||||||
|
- `out/`, `dist/`, `.next/`, `target/` (build output)
|
||||||
|
- `.claude/logs/` contents (session logs)
|
||||||
|
- Auto-generated `.claude/` in subpackages
|
||||||
|
|
||||||
|
**Never ask** about generated files - just gitignore them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Add project-specific context here. This file is read by Claude Code at the start of every session.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| Purpose | Path |
|
||||||
|
|---------|------|
|
||||||
|
| Main entry | [Add path] |
|
||||||
|
| Config | [Add path] |
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Add your project's conventions here
|
||||||
18
apps/proxy/package.json
Normal file
18
apps/proxy/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@webproxy/proxy",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@webproxy/core": "workspace:*",
|
||||||
|
"@webproxy/indexer": "workspace:*",
|
||||||
|
"@webproxy/shared": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/proxy/src/index.ts
Normal file
11
apps/proxy/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* @file index
|
||||||
|
* @description WebProxy server entry point
|
||||||
|
* @layer Application
|
||||||
|
*
|
||||||
|
* Starts the proxy server, indexer, and serves cached content to network devices.
|
||||||
|
* This is a placeholder — full implementation coming in Phase 2.
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.log("WebProxy v0.1.0 — proxy server placeholder");
|
||||||
|
console.log("Run `pnpm dev` in apps/web for the landing page");
|
||||||
7
apps/proxy/tsconfig.json
Normal file
7
apps/proxy/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
41
apps/web/.gitignore
vendored
Normal file
41
apps/web/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
36
apps/web/README.md
Normal file
36
apps/web/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
18
apps/web/eslint.config.mjs
Normal file
18
apps/web/eslint.config.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
7
apps/web/next.config.ts
Normal file
7
apps/web/next.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
26
apps/web/package.json
Normal file
26
apps/web/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@webproxy/web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.1.6",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.6",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/web/postcss.config.mjs
Normal file
7
apps/web/postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
apps/web/public/file.svg
Normal file
1
apps/web/public/file.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
apps/web/public/globe.svg
Normal file
1
apps/web/public/globe.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
apps/web/public/next.svg
Normal file
1
apps/web/public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
apps/web/public/vercel.svg
Normal file
1
apps/web/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
apps/web/public/window.svg
Normal file
1
apps/web/public/window.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
BIN
apps/web/src/app/favicon.ico
Normal file
BIN
apps/web/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
apps/web/src/app/globals.css
Normal file
26
apps/web/src/app/globals.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
41
apps/web/src/app/layout.tsx
Normal file
41
apps/web/src/app/layout.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @file layout
|
||||||
|
* @description Root layout for the WebProxy landing page
|
||||||
|
* @layer UI Component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "WebProxy - Your Local Internet Layer",
|
||||||
|
description:
|
||||||
|
"Self-hosted web indexing proxy that crawls, caches, and serves internet content to every device on your network.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
309
apps/web/src/app/page.tsx
Normal file
309
apps/web/src/app/page.tsx
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
/**
|
||||||
|
* @file page
|
||||||
|
* @description Landing page for WebProxy
|
||||||
|
* @layer UI Component
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-950 text-zinc-100">
|
||||||
|
{/* Nav */}
|
||||||
|
<nav className="fixed top-0 z-50 w-full border-b border-zinc-800 bg-zinc-950/80 backdrop-blur-sm">
|
||||||
|
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500 font-mono text-sm font-bold text-zinc-950">
|
||||||
|
WP
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-semibold">WebProxy</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-6 text-sm">
|
||||||
|
<a href="#features" className="text-zinc-400 hover:text-zinc-100 transition-colors">Features</a>
|
||||||
|
<a href="#how-it-works" className="text-zinc-400 hover:text-zinc-100 transition-colors">How It Works</a>
|
||||||
|
<a href="#get-started" className="text-zinc-400 hover:text-zinc-100 transition-colors">Get Started</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sovtech/webproxy"
|
||||||
|
className="rounded-lg bg-emerald-500 px-4 py-2 font-medium text-zinc-950 transition-colors hover:bg-emerald-400"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="relative flex min-h-screen items-center justify-center px-6 pt-16">
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-emerald-900/20 via-zinc-950 to-zinc-950" />
|
||||||
|
<div className="relative z-10 mx-auto max-w-4xl text-center">
|
||||||
|
<div className="mb-6 inline-block rounded-full border border-emerald-500/30 bg-emerald-500/10 px-4 py-1.5 text-sm text-emerald-400">
|
||||||
|
Self-hosted · Open Source · Privacy First
|
||||||
|
</div>
|
||||||
|
<h1 className="mb-6 text-5xl font-bold leading-tight tracking-tight sm:text-7xl">
|
||||||
|
Your Local
|
||||||
|
<br />
|
||||||
|
<span className="text-emerald-400">Internet Layer</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto mb-10 max-w-2xl text-lg leading-relaxed text-zinc-400 sm:text-xl">
|
||||||
|
WebProxy crawls, indexes, and caches the web for topics you care about —
|
||||||
|
then serves it all to every device on your network. Fast, private, always available.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||||
|
<a
|
||||||
|
href="#get-started"
|
||||||
|
className="rounded-lg bg-emerald-500 px-8 py-3 text-lg font-semibold text-zinc-950 transition-colors hover:bg-emerald-400"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#how-it-works"
|
||||||
|
className="rounded-lg border border-zinc-700 px-8 py-3 text-lg font-semibold text-zinc-300 transition-colors hover:border-zinc-500 hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal preview */}
|
||||||
|
<div className="mx-auto mt-16 max-w-2xl overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900 text-left font-mono text-sm shadow-2xl">
|
||||||
|
<div className="flex items-center gap-2 border-b border-zinc-800 px-4 py-3">
|
||||||
|
<div className="h-3 w-3 rounded-full bg-red-500/80" />
|
||||||
|
<div className="h-3 w-3 rounded-full bg-yellow-500/80" />
|
||||||
|
<div className="h-3 w-3 rounded-full bg-green-500/80" />
|
||||||
|
<span className="ml-2 text-xs text-zinc-500">terminal</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 p-4 text-zinc-400">
|
||||||
|
<p><span className="text-emerald-400">$</span> webproxy start --topics "machine-learning,rust,security"</p>
|
||||||
|
<p className="text-zinc-500">
|
||||||
|
[info] Proxy listening on 0.0.0.0:8080
|
||||||
|
</p>
|
||||||
|
<p className="text-zinc-500">
|
||||||
|
[info] Indexer started — crawling 3 topics
|
||||||
|
</p>
|
||||||
|
<p className="text-zinc-500">
|
||||||
|
[info] Cached 1,247 pages (342 MB)
|
||||||
|
</p>
|
||||||
|
<p className="text-zinc-500">
|
||||||
|
[info] Serving to 6 devices on 192.168.1.0/24
|
||||||
|
</p>
|
||||||
|
<p><span className="text-emerald-400">$</span> <span className="animate-pulse">_</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<section id="features" className="border-t border-zinc-800 px-6 py-24">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<h2 className="mb-4 text-center text-3xl font-bold sm:text-4xl">
|
||||||
|
Everything you need to own your internet
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mb-16 max-w-2xl text-center text-lg text-zinc-400">
|
||||||
|
WebProxy sits between your network and the internet, intelligently caching
|
||||||
|
and indexing content so your devices get fast, reliable access.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<FeatureCard
|
||||||
|
icon="🌐"
|
||||||
|
title="Topic-Based Indexing"
|
||||||
|
description="Configure topics of interest and WebProxy crawls and indexes relevant content automatically. Machine learning, news, documentation — whatever matters to you."
|
||||||
|
/>
|
||||||
|
<FeatureCard
|
||||||
|
icon="⚡"
|
||||||
|
title="Instant Local Access"
|
||||||
|
description="Cached content serves at LAN speed. No round trips to distant servers. Your whole network benefits from every page fetched."
|
||||||
|
/>
|
||||||
|
<FeatureCard
|
||||||
|
icon="🔒"
|
||||||
|
title="Privacy First"
|
||||||
|
description="All data stays on your hardware. No cloud accounts, no tracking, no data mining. Your browsing habits are yours alone."
|
||||||
|
/>
|
||||||
|
<FeatureCard
|
||||||
|
icon="🔄"
|
||||||
|
title="Smart Freshness"
|
||||||
|
description="Configurable crawl schedules keep content updated. Set aggressive refresh for news, relaxed for docs. You control the cadence."
|
||||||
|
/>
|
||||||
|
<FeatureCard
|
||||||
|
icon="🔍"
|
||||||
|
title="Local Search"
|
||||||
|
description="Full-text search across all cached content. Find what you need without hitting external search engines."
|
||||||
|
/>
|
||||||
|
<FeatureCard
|
||||||
|
icon="📡"
|
||||||
|
title="Network-Wide Proxy"
|
||||||
|
description="Any device on your network can use WebProxy as their HTTP proxy. Works with phones, tablets, laptops, IoT — anything with a browser."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<section id="how-it-works" className="border-t border-zinc-800 px-6 py-24">
|
||||||
|
<div className="mx-auto max-w-4xl">
|
||||||
|
<h2 className="mb-4 text-center text-3xl font-bold sm:text-4xl">
|
||||||
|
How it works
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mb-16 max-w-2xl text-center text-lg text-zinc-400">
|
||||||
|
Three simple steps to take control of your network's internet access.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-12">
|
||||||
|
<Step
|
||||||
|
number="1"
|
||||||
|
title="Install & Configure"
|
||||||
|
description="Deploy WebProxy on any device on your network — a Raspberry Pi, NAS, old laptop, or server. Configure your topics of interest and crawl schedules."
|
||||||
|
/>
|
||||||
|
<Step
|
||||||
|
number="2"
|
||||||
|
title="Crawl & Index"
|
||||||
|
description="WebProxy fetches, indexes, and caches web content for your configured topics. It builds a local search index so you can find anything instantly."
|
||||||
|
/>
|
||||||
|
<Step
|
||||||
|
number="3"
|
||||||
|
title="Point Your Devices"
|
||||||
|
description="Set WebProxy as the HTTP proxy for your devices (or configure your router's DNS). Every device on the network now gets fast, cached responses for indexed content."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Architecture */}
|
||||||
|
<section className="border-t border-zinc-800 px-6 py-24">
|
||||||
|
<div className="mx-auto max-w-4xl">
|
||||||
|
<h2 className="mb-4 text-center text-3xl font-bold sm:text-4xl">
|
||||||
|
Architecture
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mb-12 max-w-2xl text-center text-lg text-zinc-400">
|
||||||
|
A clean, modular design built as a monorepo.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900 font-mono text-sm">
|
||||||
|
<div className="border-b border-zinc-800 px-4 py-3 text-xs text-zinc-500">
|
||||||
|
project structure
|
||||||
|
</div>
|
||||||
|
<pre className="overflow-x-auto p-4 text-zinc-400">
|
||||||
|
{`webproxy/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Landing page & admin dashboard (Next.js)
|
||||||
|
│ └── proxy/ # Core proxy server
|
||||||
|
├── packages/
|
||||||
|
│ ├── core/ # Proxy engine & HTTP handling
|
||||||
|
│ ├── indexer/ # Web crawling & content indexing
|
||||||
|
│ └── shared/ # Shared types & utilities
|
||||||
|
├── docs/ # Deployment & usage docs
|
||||||
|
└── deploy/ # Deployment scripts & configs`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 grid gap-4 sm:grid-cols-3">
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900 p-6">
|
||||||
|
<h3 className="mb-2 font-semibold text-emerald-400">Proxy Server</h3>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Intercepts HTTP requests, checks local cache, serves cached or fetches fresh. Transparent to clients.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900 p-6">
|
||||||
|
<h3 className="mb-2 font-semibold text-emerald-400">Indexer</h3>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Background crawler that fetches content for configured topics. Builds full-text search index.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900 p-6">
|
||||||
|
<h3 className="mb-2 font-semibold text-emerald-400">Dashboard</h3>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Web UI for managing topics, monitoring cache stats, searching indexed content, and configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Get Started */}
|
||||||
|
<section id="get-started" className="border-t border-zinc-800 px-6 py-24">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
<h2 className="mb-4 text-center text-3xl font-bold sm:text-4xl">
|
||||||
|
Get started in minutes
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mb-12 max-w-xl text-center text-lg text-zinc-400">
|
||||||
|
Clone, configure, and run. That's it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900 font-mono text-sm shadow-2xl">
|
||||||
|
<div className="flex items-center gap-2 border-b border-zinc-800 px-4 py-3">
|
||||||
|
<div className="h-3 w-3 rounded-full bg-red-500/80" />
|
||||||
|
<div className="h-3 w-3 rounded-full bg-yellow-500/80" />
|
||||||
|
<div className="h-3 w-3 rounded-full bg-green-500/80" />
|
||||||
|
<span className="ml-2 text-xs text-zinc-500">setup</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 p-4 text-zinc-400">
|
||||||
|
<p className="text-zinc-500"># Clone the repo</p>
|
||||||
|
<p><span className="text-emerald-400">$</span> git clone https://git.yourdomain.com/webproxy.git</p>
|
||||||
|
<p><span className="text-emerald-400">$</span> cd webproxy</p>
|
||||||
|
<p> </p>
|
||||||
|
<p className="text-zinc-500"># Install dependencies</p>
|
||||||
|
<p><span className="text-emerald-400">$</span> pnpm install</p>
|
||||||
|
<p> </p>
|
||||||
|
<p className="text-zinc-500"># Start the proxy</p>
|
||||||
|
<p><span className="text-emerald-400">$</span> pnpm dev</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="border-t border-zinc-800 px-6 py-12">
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-4 sm:flex-row">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded bg-emerald-500 font-mono text-xs font-bold text-zinc-950">
|
||||||
|
WP
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-zinc-400">WebProxy — Own your internet.</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-6 text-sm text-zinc-500">
|
||||||
|
<a href="https://github.com/sovtech/webproxy" className="hover:text-zinc-300 transition-colors">GitHub</a>
|
||||||
|
<a href="/docs" className="hover:text-zinc-300 transition-colors">Docs</a>
|
||||||
|
<span>MIT License</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeatureCard({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900 p-6 transition-colors hover:border-zinc-700">
|
||||||
|
<div className="mb-4 text-3xl">{icon}</div>
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">{title}</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-zinc-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step({
|
||||||
|
number,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
number: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-emerald-500/30 bg-emerald-500/10 font-mono text-lg font-bold text-emerald-400">
|
||||||
|
{number}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-xl font-semibold">{title}</h3>
|
||||||
|
<p className="text-zinc-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
apps/web/tsconfig.json
Normal file
34
apps/web/tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
98
docs/ARCHITECTURE.md
Normal file
98
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# WebProxy Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
WebProxy is a self-hosted web indexing proxy that crawls, caches, and serves internet content to devices on your local network.
|
||||||
|
|
||||||
|
## Recommended Tech Stack
|
||||||
|
|
||||||
|
Based on research into cutting-edge open source tools, the following stack is recommended for a TypeScript/Node.js monorepo:
|
||||||
|
|
||||||
|
### Tier 1: Core (Native TypeScript)
|
||||||
|
|
||||||
|
| Component | Tool | npm Package | Role |
|
||||||
|
|-----------|------|-------------|------|
|
||||||
|
| Crawler | Crawlee | `crawlee`, `@crawlee/playwright` | Crawl/index topics of interest |
|
||||||
|
| Forward Proxy | http-proxy-3 | `http-proxy-3` | Transparent caching proxy for network devices |
|
||||||
|
| MITM Proxy | mockttp | `mockttp` | HTTPS interception, request/response capture |
|
||||||
|
| WARC Storage | warcio.js | `warcio` | Write/read WARC archives of cached content |
|
||||||
|
| Search | MeiliSearch | `meilisearch` (client) | Full-text search of indexed content |
|
||||||
|
| Dashboard | Next.js | `next` | Web dashboard for management |
|
||||||
|
|
||||||
|
### Tier 2: Utility Libraries
|
||||||
|
|
||||||
|
| Component | Package | Role |
|
||||||
|
|-----------|---------|------|
|
||||||
|
| Content extraction | `@mozilla/readability` | Extract article content from HTML |
|
||||||
|
| HTML to Markdown | `turndown` | Convert HTML to Markdown for AI/LLM use |
|
||||||
|
| DOM parsing | `cheerio` | Server-side HTML parsing |
|
||||||
|
| Caching layer | `keyv` or custom LRU | HTTP response caching with TTL |
|
||||||
|
|
||||||
|
### Tier 3: Optional Sidecars (Docker)
|
||||||
|
|
||||||
|
| Component | Tool | Role |
|
||||||
|
|-----------|------|------|
|
||||||
|
| Replay engine | pywb | Serve archived WARC content Wayback-style |
|
||||||
|
| Deep archiving | ArchiveBox | Comprehensive page archiving |
|
||||||
|
|
||||||
|
## Why These Choices
|
||||||
|
|
||||||
|
### Crawlee over Scrapy/Nutch/Crawl4AI
|
||||||
|
- **Native TypeScript** - single language across the monorepo
|
||||||
|
- Supports Puppeteer, Playwright, Cheerio, and raw HTTP crawlers
|
||||||
|
- Built-in request queue, proxy rotation, autoscaling
|
||||||
|
- Actively maintained by Apify (monthly releases)
|
||||||
|
|
||||||
|
### http-proxy-3 + mockttp over Squid/mitmproxy
|
||||||
|
- Pure Node.js - no external binary management
|
||||||
|
- `http-proxy-3` is a modern rewrite fixing socket leaks, partial HTTP/2
|
||||||
|
- `mockttp` provides full MITM capabilities natively in TypeScript
|
||||||
|
|
||||||
|
### MeiliSearch over OpenSearch/YaCy/Solr
|
||||||
|
- Rust binary - lightweight, runs on Raspberry Pi (~1GB RAM)
|
||||||
|
- Official Node.js SDK with TypeScript types
|
||||||
|
- Auto language detection, typo tolerance
|
||||||
|
- Single binary deployment
|
||||||
|
|
||||||
|
### warcio.js over warcprox
|
||||||
|
- Native TypeScript (v2.0+) by the Webrecorder team
|
||||||
|
- Streaming WARC read/write for both browser and Node.js
|
||||||
|
- No Python dependency
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Network Device Internet
|
||||||
|
| |
|
||||||
|
| HTTP(S) request |
|
||||||
|
v |
|
||||||
|
[proxy] |
|
||||||
|
| |
|
||||||
|
|--> Cache hit? --> [storage] --> Serve from WARC
|
||||||
|
| |
|
||||||
|
|--> Cache miss? --------------> Fetch from Internet
|
||||||
|
| | |
|
||||||
|
| +--> [storage] ------> Write to WARC
|
||||||
|
| +--> [indexer] ------> Index in MeiliSearch
|
||||||
|
| |
|
||||||
|
v |
|
||||||
|
Response to Device |
|
||||||
|
|
|
||||||
|
[crawler] (scheduled) ------------> Crawl topics
|
||||||
|
| |
|
||||||
|
+--> [storage] ----------------> Write to WARC
|
||||||
|
+--> [indexer] ----------------> Index in MeiliSearch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
core/ # http-proxy-3 + mockttp based forward proxy
|
||||||
|
indexer/ # Crawlee-based topic crawler + MeiliSearch indexing
|
||||||
|
shared/ # Shared types, utilities, config schemas
|
||||||
|
|
||||||
|
apps/
|
||||||
|
web/ # Next.js landing page & admin dashboard
|
||||||
|
proxy/ # Main proxy server entry point
|
||||||
|
```
|
||||||
109
docs/DEPLOYMENT.md
Normal file
109
docs/DEPLOYMENT.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# WebProxy Deployment Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js >= 20
|
||||||
|
- pnpm >= 9
|
||||||
|
- Git
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repo
|
||||||
|
git clone git@185.191.239.154:jeremy/webproxy.git
|
||||||
|
cd webproxy
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Start development server (landing page)
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Start production server
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Remote (Gitea)
|
||||||
|
|
||||||
|
The project is hosted on a self-hosted Gitea instance:
|
||||||
|
|
||||||
|
- **Web UI**: http://185.191.239.154:3000/jeremy/webproxy
|
||||||
|
- **SSH Clone**: `git@185.191.239.154:jeremy/webproxy.git`
|
||||||
|
- **HTTP Clone**: `http://185.191.239.154:3000/jeremy/webproxy.git`
|
||||||
|
|
||||||
|
### Setup SSH Access
|
||||||
|
|
||||||
|
1. Generate an SSH key if you don't have one:
|
||||||
|
```bash
|
||||||
|
ssh-keygen -t ed25519 -C "your@email.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add your public key to Gitea:
|
||||||
|
- Go to http://185.191.239.154:3000/user/settings/keys
|
||||||
|
- Click "Add Key" and paste your `~/.ssh/id_ed25519.pub`
|
||||||
|
|
||||||
|
3. Add the remote:
|
||||||
|
```bash
|
||||||
|
git remote add origin git@185.191.239.154:jeremy/webproxy.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pushing Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: your changes"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monorepo Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
webproxy/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js landing page & dashboard
|
||||||
|
│ └── proxy/ # Core proxy server (future)
|
||||||
|
├── packages/
|
||||||
|
│ ├── core/ # Proxy engine & HTTP handling
|
||||||
|
│ ├── indexer/ # Web crawling & indexing
|
||||||
|
│ └── shared/ # Shared types & utilities
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── deploy/ # Deployment configs
|
||||||
|
└── specs/ # Feature specifications
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building Individual Packages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build just the web app
|
||||||
|
pnpm --filter @webproxy/web build
|
||||||
|
|
||||||
|
# Run type checking across all packages
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
# Clean all build artifacts
|
||||||
|
pnpm clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Deployment (Future)
|
||||||
|
|
||||||
|
Deployment to the Gitea server will use:
|
||||||
|
|
||||||
|
1. **Git hooks** - Post-receive hooks for auto-deployment
|
||||||
|
2. **Docker** - Container-based deployment for the proxy service
|
||||||
|
3. **systemd** - Service management for the proxy daemon
|
||||||
|
|
||||||
|
### Planned Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Server (185.191.239.154) │
|
||||||
|
├──────────────────────────────────────────┤
|
||||||
|
│ Gitea → Git hosting (port 3000) │
|
||||||
|
│ WebProxy Web → Landing page (port 3001) │
|
||||||
|
│ WebProxy → Proxy server (port 8080) │
|
||||||
|
│ MeiliSearch → Search engine (port 7700) │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "webproxy",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Local internet indexing layer - crawl, cache, and serve web content to your network",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "pnpm --filter @webproxy/web dev",
|
||||||
|
"build": "pnpm --filter @webproxy/web build",
|
||||||
|
"start": "pnpm --filter @webproxy/web start",
|
||||||
|
"lint": "pnpm -r lint",
|
||||||
|
"clean": "pnpm -r clean",
|
||||||
|
"typecheck": "pnpm -r typecheck"
|
||||||
|
},
|
||||||
|
"keywords": ["proxy", "web-indexer", "cache", "local-network"],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20",
|
||||||
|
"pnpm": ">=9"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.24.0"
|
||||||
|
}
|
||||||
14
packages/core/package.json
Normal file
14
packages/core/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@webproxy/core",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@webproxy/shared": "workspace:*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/core/src/index.ts
Normal file
7
packages/core/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file index
|
||||||
|
* @description Core proxy engine - HTTP handling and caching
|
||||||
|
* @layer Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { type ProxyConfig, type CachedPage } from "@webproxy/shared";
|
||||||
7
packages/core/tsconfig.json
Normal file
7
packages/core/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
14
packages/indexer/package.json
Normal file
14
packages/indexer/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@webproxy/indexer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@webproxy/shared": "workspace:*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/indexer/src/index.ts
Normal file
7
packages/indexer/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file index
|
||||||
|
* @description Web crawling and indexing engine
|
||||||
|
* @layer Indexer
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { type TopicConfig } from "@webproxy/shared";
|
||||||
7
packages/indexer/tsconfig.json
Normal file
7
packages/indexer/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
11
packages/shared/package.json
Normal file
11
packages/shared/package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@webproxy/shared",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
packages/shared/src/index.ts
Normal file
31
packages/shared/src/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* @file index
|
||||||
|
* @description Shared types and utilities for the webproxy monorepo
|
||||||
|
* @layer Shared
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TopicConfig = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
keywords: string[];
|
||||||
|
urls: string[];
|
||||||
|
crawlInterval: number; // minutes
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CachedPage = {
|
||||||
|
url: string;
|
||||||
|
contentHash: string;
|
||||||
|
fetchedAt: Date;
|
||||||
|
expiresAt: Date;
|
||||||
|
contentType: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProxyConfig = {
|
||||||
|
port: number;
|
||||||
|
hostname: string;
|
||||||
|
cacheDir: string;
|
||||||
|
maxCacheSize: number; // bytes
|
||||||
|
topics: TopicConfig[];
|
||||||
|
};
|
||||||
7
packages/shared/tsconfig.json
Normal file
7
packages/shared/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
4053
pnpm-lock.yaml
generated
Normal file
4053
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
84
specs/webproxy.md
Normal file
84
specs/webproxy.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# WebProxy - Local Internet Indexing Layer
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
WebProxy is a self-hosted program that runs on a local device and acts as a web internet indexing layer. It crawls, caches, and indexes web content for any topics of interest, then serves that cached content to other devices on the local network as if it were the live internet.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
- Internet access can be slow, metered, unreliable, or censored
|
||||||
|
- Multiple devices on a network redundantly fetch the same content
|
||||||
|
- No local control over what content is available or prioritized
|
||||||
|
- Search results depend on external providers with their own agendas
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
A local proxy/indexer that:
|
||||||
|
1. **Crawls & Indexes** - Fetches web pages, search results, and content based on configured topics of interest
|
||||||
|
2. **Caches Locally** - Stores all fetched content in a local database/filesystem
|
||||||
|
3. **Serves to Network** - Acts as a proxy/DNS for other devices, serving cached content as if it were live internet
|
||||||
|
4. **Stays Fresh** - Periodically re-crawls to keep content updated based on configurable schedules
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
webproxy/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js landing page & admin dashboard
|
||||||
|
│ └── proxy/ # Core proxy server (serves content to network devices)
|
||||||
|
├── packages/
|
||||||
|
│ ├── core/ # Proxy engine & HTTP handling
|
||||||
|
│ ├── indexer/ # Web crawling & indexing engine
|
||||||
|
│ └── shared/ # Shared types, utils, config schemas
|
||||||
|
├── docs/ # Deployment & usage documentation
|
||||||
|
└── deploy/ # Deployment scripts & configs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
1. **Proxy Server** (`apps/proxy`) - HTTP/HTTPS proxy that intercepts requests, checks local cache, serves cached content or fetches fresh
|
||||||
|
2. **Indexer** (`packages/indexer`) - Crawls configured topics, indexes content, stores in local DB
|
||||||
|
3. **Web Dashboard** (`apps/web`) - Next.js app for configuration, monitoring, topic management
|
||||||
|
4. **Core Engine** (`packages/core`) - Shared proxy logic, caching strategies, content transformation
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Network Device] → [WebProxy Proxy Server] → [Local Cache]
|
||||||
|
↓ (cache miss)
|
||||||
|
[Live Internet]
|
||||||
|
↓
|
||||||
|
[Cache & Index]
|
||||||
|
↓
|
||||||
|
[Serve to Device]
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
- As a network admin, I want to configure topics of interest so the proxy pre-fetches relevant content
|
||||||
|
- As a device user, I want to browse the web through the proxy and get fast cached responses
|
||||||
|
- As a network admin, I want to see what content is cached and manage storage
|
||||||
|
- As a device user, I want search results that include locally cached content
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### Landing Page (Phase 1 - Current)
|
||||||
|
- [x] Monorepo structure with pnpm workspaces
|
||||||
|
- [ ] Next.js landing page explaining the product
|
||||||
|
- [ ] Documentation for deployment
|
||||||
|
- [ ] Gitea remote configured for CI/CD
|
||||||
|
- [ ] Clean, professional landing page with feature overview
|
||||||
|
|
||||||
|
### Core Proxy (Phase 2 - Future)
|
||||||
|
- [ ] HTTP proxy server that intercepts requests
|
||||||
|
- [ ] Local content cache with configurable storage
|
||||||
|
- [ ] Topic-based crawling configuration
|
||||||
|
- [ ] Web dashboard for management
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- Self-hosted via Gitea on `185.191.239.154`
|
||||||
|
- Git-based deployment workflow
|
||||||
|
- Docker support planned for Phase 2
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", ".next", "out"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user