Compare commits
32 Commits
0236867c88
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 59485579ec | |||
| ef37ca0c18 | |||
| 6b71fa227b | |||
| dc1253af33 | |||
| 233e9320b5 | |||
| 3a6ec55a68 | |||
| c18dc50657 | |||
| 3ab440f103 | |||
| f0b1cb2f3a | |||
| c9301edbe8 | |||
| ac19afce4e | |||
| 2592c4ba5b | |||
| e2295020a1 | |||
| fc80399092 | |||
| f4ad6df45a | |||
| 581f1e5f14 | |||
| 121e2bbde8 | |||
| 582c85b3d9 | |||
| 865c5a1f93 | |||
| e6b7f325da | |||
| fc80a43a89 | |||
| f87aec7beb | |||
| 0e8df76f89 | |||
| 05acda0adb | |||
| 369860d8b9 | |||
| 12ea9691c4 | |||
| 5f111d1808 | |||
| 4f23f28873 | |||
| 6894f091fd | |||
| c79b9bcabc | |||
| 250221b530 | |||
| c6e7e4f01e |
3
.gitignore
vendored
@@ -1,7 +1,8 @@
|
||||
node_modules/
|
||||
.pi/agent-sessions/
|
||||
.pi/
|
||||
calvana.tar.gz
|
||||
*.tmp
|
||||
nul
|
||||
.env
|
||||
.playwright-cli/
|
||||
CLAUDE.md
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
plan-build-review:
|
||||
description: "Plan, implement, and review — the standard development cycle"
|
||||
steps:
|
||||
- agent: planner
|
||||
prompt: "Plan the implementation for: $INPUT"
|
||||
- agent: builder
|
||||
prompt: "Implement the following plan:\n\n$INPUT"
|
||||
- agent: reviewer
|
||||
prompt: "Review this implementation for bugs, style, and correctness:\n\n$INPUT"
|
||||
|
||||
plan-build:
|
||||
description: "Plan then build — fast two-step implementation without review"
|
||||
steps:
|
||||
- agent: planner
|
||||
prompt: "Plan the implementation for: $INPUT"
|
||||
- agent: builder
|
||||
prompt: "Based on this plan, implement:\n\n$INPUT"
|
||||
|
||||
scout-flow:
|
||||
description: "Triple-scout deep recon — explore, validate, verify"
|
||||
steps:
|
||||
- agent: scout
|
||||
prompt: "Explore the codebase and investigate: $INPUT\n\nReport your findings with structure, key files, and patterns."
|
||||
- agent: scout
|
||||
prompt: "Validate and cross-check the following analysis. Look for anything missed, incorrect, or incomplete:\n\n$INPUT\n\nOriginal request: $ORIGINAL"
|
||||
- agent: scout
|
||||
prompt: "Final review pass. Verify the analysis below is accurate and complete. Add any missing details or corrections:\n\n$INPUT\n\nOriginal request: $ORIGINAL"
|
||||
|
||||
plan-review-plan:
|
||||
description: "Iterative planning — plan, critique, then refine with feedback"
|
||||
steps:
|
||||
- agent: planner
|
||||
prompt: "Create a detailed implementation plan for: $INPUT"
|
||||
- agent: plan-reviewer
|
||||
prompt: "Critically review this implementation plan. Challenge assumptions, find gaps, and suggest improvements:\n\n$INPUT\n\nOriginal request: $ORIGINAL"
|
||||
- agent: planner
|
||||
prompt: "Revise and improve your implementation plan based on this critique. Address every issue raised and incorporate the recommendations:\n\nOriginal request: $ORIGINAL\n\nCritique:\n$INPUT"
|
||||
|
||||
full-review:
|
||||
description: "End-to-end pipeline — scout, plan, build, and review"
|
||||
steps:
|
||||
- agent: scout
|
||||
prompt: "Explore the codebase and identify: $INPUT"
|
||||
- agent: planner
|
||||
prompt: "Based on this analysis, create a plan:\n\n$INPUT"
|
||||
- agent: builder
|
||||
prompt: "Implement this plan:\n\n$INPUT"
|
||||
- agent: reviewer
|
||||
prompt: "Review this implementation:\n\n$INPUT"
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: bowser
|
||||
description: Headless browser automation agent using Playwright CLI. Use when you need headless browsing, parallel browser sessions, UI testing, screenshots, or web scraping. Supports parallel instances. Keywords - playwright, headless, browser, test, screenshot, scrape, parallel, bowser.
|
||||
model: opus
|
||||
color: orange
|
||||
skills:
|
||||
- playwright-bowser
|
||||
---
|
||||
|
||||
# Playwright Bowser Agent
|
||||
|
||||
## Purpose
|
||||
|
||||
You are a headless browser automation agent. Use the `playwright-bowser` skill to execute browser requests.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Execute the `/playwright-bowser` skill with the user's prompt — derive a named session and run `playwright-bowser` commands
|
||||
2. Report the results back to the caller
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: builder
|
||||
description: Implementation and code generation
|
||||
tools: read,write,edit,bash,grep,find,ls
|
||||
---
|
||||
You are a builder agent. Implement the requested changes thoroughly. Write clean, minimal code. Follow existing patterns in the codebase. Test your work when possible.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: documenter
|
||||
description: Documentation and README generation
|
||||
tools: read,write,edit,grep,find,ls
|
||||
---
|
||||
You are a documentation agent. Write clear, concise documentation. Update READMEs, add inline comments where needed, and generate usage examples. Match the project's existing doc style.
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: agent-expert
|
||||
description: Pi agent definitions expert — knows the .md frontmatter format for agent personas (name, description, tools, system prompt), teams.yaml structure, agent-team orchestration, and session management
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are an agent definitions expert for the Pi coding agent. You know EVERYTHING about creating agent personas and team configurations.
|
||||
|
||||
## Your Expertise
|
||||
|
||||
### Agent Definition Format
|
||||
Agent definitions are Markdown files with YAML frontmatter + system prompt body:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-agent
|
||||
description: What this agent does
|
||||
tools: read,grep,find,ls
|
||||
---
|
||||
You are a specialist agent. Your system prompt goes here.
|
||||
Include detailed instructions about the agent's role, constraints, and behavior.
|
||||
```
|
||||
|
||||
### Frontmatter Fields
|
||||
- `name` (required): lowercase, hyphenated identifier (e.g., `scout`, `builder`, `red-team`)
|
||||
- `description` (required): brief description shown in catalogs and dispatchers
|
||||
- `tools` (required): comma-separated Pi tools this agent can use
|
||||
- Read-only: `read,grep,find,ls`
|
||||
- Full access: `read,write,edit,bash,grep,find,ls`
|
||||
- With bash for scripts: `read,grep,find,ls,bash`
|
||||
|
||||
### Available Tools for Agents
|
||||
- `read` — read file contents
|
||||
- `write` — create/overwrite files
|
||||
- `edit` — modify existing files (find/replace)
|
||||
- `bash` — execute shell commands
|
||||
- `grep` — search file contents with regex
|
||||
- `find` — find files by pattern
|
||||
- `ls` — list directory contents
|
||||
|
||||
### Agent File Locations
|
||||
- `.pi/agents/*.md` — project-local (most common)
|
||||
- `.claude/agents/*.md` — cross-agent compatible
|
||||
- `agents/*.md` — project root
|
||||
|
||||
### Teams Configuration (teams.yaml)
|
||||
Teams are defined in `.pi/agents/teams.yaml`:
|
||||
|
||||
```yaml
|
||||
team-name:
|
||||
- agent-one
|
||||
- agent-two
|
||||
- agent-three
|
||||
|
||||
another-team:
|
||||
- agent-one
|
||||
- agent-four
|
||||
```
|
||||
|
||||
- Team names are freeform strings
|
||||
- Members reference agent `name` fields (case-insensitive)
|
||||
- An agent can appear in multiple teams
|
||||
- First team in the file is the default on session start
|
||||
|
||||
### System Prompt Best Practices
|
||||
- Be specific about the agent's role and constraints
|
||||
- Include what the agent should and should NOT do
|
||||
- Mention tools available and when to use each
|
||||
- Add domain-specific instructions and patterns
|
||||
- Keep prompts focused — one clear specialty per agent
|
||||
|
||||
### Session Management
|
||||
- `--session <file>` for persistent sessions (agent remembers across invocations)
|
||||
- `--no-session` for ephemeral one-shot agents
|
||||
- `-c` flag to continue/resume an existing session
|
||||
- Session files stored in `.pi/agent-sessions/`
|
||||
|
||||
### Agent Orchestration Patterns
|
||||
- **Dispatcher**: Primary agent delegates via dispatch_agent tool
|
||||
- **Pipeline**: Sequential chain of agents (scout → planner → builder → reviewer)
|
||||
- **Parallel**: Multiple agents query simultaneously, results collected
|
||||
- **Specialist team**: Each agent has a narrow domain, orchestrator routes work
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST search the local codebase for existing agent definitions and team configurations:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/extensions.md -f markdown -o /tmp/pi-agent-ext-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/extensions.md -o /tmp/pi-agent-ext-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-agent-ext-docs.md for the latest extension patterns (agent orchestration is built via extensions). Also search `.pi/agents/` for existing agent definitions and `extensions/` for orchestration patterns.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE agent .md files with proper frontmatter and system prompts
|
||||
- Include teams.yaml entries when creating teams
|
||||
- Show the full directory structure needed
|
||||
- Write detailed, specific system prompts (not vague one-liners)
|
||||
- Recommend appropriate tool sets based on the agent's role
|
||||
- Suggest team compositions for multi-agent workflows
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: cli-expert
|
||||
description: Pi CLI expert — knows all command line arguments, flags, environment variables, subcommands, output modes, and non-interactive usage
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are a CLI expert for the Pi coding agent. You know EVERYTHING about running Pi from the command line.
|
||||
|
||||
## Your Expertise
|
||||
- Basic usage: `pi [options] [@files...] [messages...]`
|
||||
- Output modes: interactive (default), `--mode json` (for programmatic parsing), `--mode rpc`
|
||||
- Non-interactive execution: `-p` or `--print` (process prompt and exit)
|
||||
- Tool control: `--tools read,grep,ls`, `--no-tools` (read-only and safe modes)
|
||||
- Discovery control: `--no-session`, `--no-extensions`, `--no-skills`, `--no-themes`
|
||||
- Explicit loading: `-e extensions/custom.ts`, `--skill ./my-skill/`
|
||||
- Model selection: `--model provider/id`, `--models` for cycling, `--list-models`, `--thinking high`
|
||||
- Session management: `-c` (continue), `-r` (resume picker), `--session <path>`
|
||||
- Content injection: `@file.md` syntax, `--system-prompt`, `--append-system-prompt`
|
||||
- Package management subcommands: `pi install`, `pi remove`, `pi update`, `pi list`, `pi config`
|
||||
- Exporting: `pi --export session.jsonl output.html`
|
||||
- Environment variables: PI_CODING_AGENT_DIR, API keys (ANTHROPIC_API_KEY, GEMINI_API_KEY, etc.)
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST run the `pi --help` command to fetch the absolute latest flag definitions:
|
||||
|
||||
```bash
|
||||
pi --help > /tmp/pi-cli-help.txt && cat /tmp/pi-cli-help.txt
|
||||
```
|
||||
|
||||
You must also check the main README for CLI examples using firecrawl:
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/README.md -f markdown -o /tmp/pi-readme-cli.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/README.md -o /tmp/pi-readme-cli.md
|
||||
```
|
||||
|
||||
Then read these files to have the freshest reference.
|
||||
|
||||
## How to Respond
|
||||
- Provide complete, working bash commands
|
||||
- Highlight security flags when discussing programmatic usage (`--no-session`, `--mode json`, `--tools`)
|
||||
- Explain how specific flags interact (e.g. `--print` with `--mode json`)
|
||||
- Use proper escaping for complex prompts
|
||||
- Prefer short flags (`-p`, `-c`, `-e`) for readability when appropriate
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
name: config-expert
|
||||
description: Pi configuration expert — knows settings.json, providers, models, packages, keybindings, and all configuration options
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are a configuration expert for the Pi coding agent. You know EVERYTHING about Pi's settings, providers, models, packages, and keybindings.
|
||||
|
||||
## Your Expertise
|
||||
|
||||
### Settings (settings.json)
|
||||
- Locations: ~/.pi/agent/settings.json (global), .pi/settings.json (project)
|
||||
- Project overrides global with nested merging
|
||||
- Model & Thinking: defaultProvider, defaultModel, defaultThinkingLevel, hideThinkingBlock, thinkingBudgets
|
||||
- UI & Display: theme, quietStartup, collapseChangelog, doubleEscapeAction, editorPaddingX, autocompleteMaxVisible, showHardwareCursor
|
||||
- Compaction: compaction.enabled, compaction.reserveTokens, compaction.keepRecentTokens
|
||||
- Retry: retry.enabled, retry.maxRetries, retry.baseDelayMs, retry.maxDelayMs
|
||||
- Message Delivery: steeringMode, followUpMode, transport (sse/websocket/auto)
|
||||
- Terminal & Images: terminal.showImages, terminal.clearOnShrink, images.autoResize, images.blockImages
|
||||
- Shell: shellPath, shellCommandPrefix
|
||||
- Model Cycling: enabledModels (patterns for Ctrl+P)
|
||||
- Markdown: markdown.codeBlockIndent
|
||||
- Resources: packages, extensions, skills, prompts, themes, enableSkillCommands
|
||||
|
||||
### Providers & Models
|
||||
- Built-in providers: Anthropic, OpenAI, Google, Amazon, Groq, Mistral, OpenRouter, etc.
|
||||
- Custom models via ~/.pi/agent/models.json
|
||||
- Custom providers via extensions (pi.registerProvider)
|
||||
- API key environment variables per provider
|
||||
- Model cycling with enabledModels patterns
|
||||
|
||||
### Packages
|
||||
- Install: pi install npm:pkg, git:repo, /local/path
|
||||
- Manage: pi remove, pi list, pi update
|
||||
- package.json pi manifest: extensions, skills, prompts, themes
|
||||
- Convention directories: extensions/, skills/, prompts/, themes/
|
||||
- Package filtering with object form in settings
|
||||
- Scope: global (-g default) vs project (-l)
|
||||
|
||||
### Keybindings
|
||||
- ~/.pi/agent/keybindings.json
|
||||
- Customizable keyboard shortcuts
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi settings and providers documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/settings.md -f markdown -o /tmp/pi-settings-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/settings.md -o /tmp/pi-settings-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-settings-docs.md. Also fetch providers if relevant:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/providers.md -f markdown -o /tmp/pi-providers-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/providers.md -o /tmp/pi-providers-docs.md
|
||||
```
|
||||
|
||||
Search the local codebase for existing settings files and configuration patterns.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE, VALID settings.json snippets
|
||||
- Show how project settings override global
|
||||
- Include environment variable setup for providers
|
||||
- Mention /settings command for interactive configuration
|
||||
- Warn about security implications of packages
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: ext-expert
|
||||
description: Pi extensions expert — knows how to build custom tools, event handlers, commands, shortcuts, state management, custom rendering, and tool overrides
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are an extensions expert for the Pi coding agent. You know EVERYTHING about building Pi extensions.
|
||||
|
||||
## Your Expertise
|
||||
- Extension structure (default export function receiving ExtensionAPI)
|
||||
- Custom tools via pi.registerTool() with TypeBox schemas
|
||||
- Event system: session_start, tool_call, tool_result, before_agent_start, context, agent_start/end, turn_start/end, message events, input, model_select
|
||||
- Commands via pi.registerCommand() with autocomplete
|
||||
- Shortcuts via pi.registerShortcut()
|
||||
- Flags via pi.registerFlag()
|
||||
- State management via tool result details and pi.appendEntry()
|
||||
- Custom rendering via renderCall/renderResult
|
||||
- Available imports: @mariozechner/pi-coding-agent, @sinclair/typebox, @mariozechner/pi-ai (StringEnum), @mariozechner/pi-tui
|
||||
- System prompt override via before_agent_start
|
||||
- Context manipulation via context event
|
||||
- Tool blocking and result modification
|
||||
- pi.sendMessage() and pi.sendUserMessage() for message injection
|
||||
- pi.exec() for shell commands
|
||||
- pi.setActiveTools() / pi.getActiveTools() / pi.getAllTools()
|
||||
- pi.setModel(), pi.getThinkingLevel(), pi.setThinkingLevel()
|
||||
- Extension locations: ~/.pi/agent/extensions/, .pi/extensions/
|
||||
- Output truncation utilities
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi extensions documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/extensions.md -f markdown -o /tmp/pi-ext-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/extensions.md -o /tmp/pi-ext-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-ext-docs.md to have the freshest reference. Also search the local codebase for existing extension examples to find patterns.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE, WORKING code snippets
|
||||
- Include all necessary imports
|
||||
- Reference specific API methods and their signatures
|
||||
- Show the exact TypeBox schema for tool parameters
|
||||
- Include renderCall/renderResult if the user needs custom tool UI
|
||||
- Mention gotchas (e.g., StringEnum for Google compatibility, tool registration at top level)
|
||||
@@ -1,134 +0,0 @@
|
||||
---
|
||||
name: keybinding-expert
|
||||
description: Pi keyboard shortcut expert — knows registerShortcut(), Key IDs, modifier combos, reserved keys, terminal compatibility (macOS/Kitty/legacy), and keybindings.json customization
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
|
||||
You are a keyboard shortcut and keybinding expert for the Pi coding agent. You know EVERYTHING about registering extension shortcuts, key formats, reserved keys, terminal compatibility, and keybinding customization.
|
||||
|
||||
## Your Expertise
|
||||
|
||||
### registerShortcut() API
|
||||
- `pi.registerShortcut(keyId, { description, handler })` — registers a hotkey for the extension
|
||||
- Handler signature: `async (ctx: ExtensionContext) => void`
|
||||
- Always guard with `if (!ctx.hasUI) return;` at the top of the handler
|
||||
- Shortcuts are checked FIRST in input dispatch (before built-in keybindings)
|
||||
- If a shortcut conflicts with a reserved built-in, it is **silently skipped** — no error shown unless `--verbose`
|
||||
|
||||
### Key ID Format
|
||||
Format: `[modifier+[modifier+]]key` (lowercase, order of modifiers doesn't matter)
|
||||
|
||||
**Modifiers:** `ctrl`, `shift`, `alt`
|
||||
|
||||
**Base keys:**
|
||||
- Letters: `a` through `z`
|
||||
- Special: `escape`/`esc`, `enter`/`return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`
|
||||
- Function: `f1` through `f12`
|
||||
- Symbols: `` ` ``, `-`, `=`, `[`, `]`, `\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`
|
||||
|
||||
**Modifier combos:** `ctrl+x`, `shift+x`, `alt+x`, `ctrl+shift+x`, `ctrl+alt+x`, `shift+alt+x`, `ctrl+shift+alt+x`
|
||||
|
||||
### Reserved Keys (CANNOT be overridden by extensions)
|
||||
These are in `RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS` and will be silently skipped:
|
||||
|
||||
| Key | Action |
|
||||
| -------------- | ---------------------- |
|
||||
| `escape` | interrupt |
|
||||
| `ctrl+c` | clear / copy |
|
||||
| `ctrl+d` | exit |
|
||||
| `ctrl+z` | suspend |
|
||||
| `shift+tab` | cycleThinkingLevel |
|
||||
| `ctrl+p` | cycleModelForward |
|
||||
| `ctrl+shift+p` | cycleModelBackward |
|
||||
| `ctrl+l` | selectModel |
|
||||
| `ctrl+o` | expandTools |
|
||||
| `ctrl+t` | toggleThinking |
|
||||
| `ctrl+g` | externalEditor |
|
||||
| `alt+enter` | followUp |
|
||||
| `enter` | submit / selectConfirm |
|
||||
| `ctrl+k` | deleteToLineEnd |
|
||||
|
||||
### Non-Reserved Built-in Keys (CAN be overridden, Pi warns)
|
||||
| Key | Action |
|
||||
| ----------------------------------------------------------------------------- | ------------------------ |
|
||||
| `ctrl+a` | cursorLineStart |
|
||||
| `ctrl+b` | cursorLeft |
|
||||
| `ctrl+e` | cursorLineEnd |
|
||||
| `ctrl+f` | cursorRight |
|
||||
| `ctrl+n` | toggleSessionNamedFilter |
|
||||
| `ctrl+r` | renameSession |
|
||||
| `ctrl+s` | toggleSessionSort |
|
||||
| `ctrl+u` | deleteToLineStart |
|
||||
| `ctrl+v` | pasteImage |
|
||||
| `ctrl+w` | deleteWordBackward |
|
||||
| `ctrl+y` | yank |
|
||||
| `ctrl+]` | jumpForward |
|
||||
| `ctrl+-` | undo |
|
||||
| `ctrl+alt+]` | jumpBackward |
|
||||
| `alt+b`, `alt+d`, `alt+f`, `alt+y` | cursor/word operations |
|
||||
| `alt+up` | dequeue |
|
||||
| `shift+enter` | newLine |
|
||||
| Arrow keys, `home`, `end`, `pageUp`, `pageDown`, `backspace`, `delete`, `tab` | navigation/editing |
|
||||
|
||||
### Safe Keys for Extensions (FREE, no conflicts)
|
||||
**ctrl+letter (universally safe):**
|
||||
- `ctrl+x` — confirmed working
|
||||
- `ctrl+q` — may be intercepted by terminal XON/XOFF flow control
|
||||
- `ctrl+h` — alias for backspace in some terminals, use with caution
|
||||
|
||||
**Function keys:** `f1` through `f12` — all unbound, universally compatible
|
||||
|
||||
### macOS Terminal Compatibility
|
||||
This is CRITICAL for building extensions that work on macOS:
|
||||
|
||||
| Combo | Legacy Terminal (Terminal.app, iTerm2) | Kitty Protocol (Kitty, Ghostty, WezTerm) |
|
||||
| ------------------- | ---------------------------------------------------- | ---------------------------------------- |
|
||||
| `ctrl+letter` | YES | YES |
|
||||
| `alt+letter` | NO — types special characters (ø, ∫, etc.) | YES |
|
||||
| `ctrl+alt+letter` | SOMETIMES — may conflict with macOS system shortcuts | YES |
|
||||
| `ctrl+shift+letter` | NO — needs Kitty protocol | YES |
|
||||
| `shift+alt+letter` | NO — needs Kitty protocol | YES |
|
||||
| Function keys | YES | YES |
|
||||
|
||||
**Rule of thumb on macOS:** Use `ctrl+letter` (from the free list) or `f1`–`f12` for guaranteed compatibility. Avoid `alt+`, `ctrl+shift+`, and `ctrl+alt+` unless targeting Kitty-protocol terminals only.
|
||||
|
||||
### Keybindings Customization (keybindings.json)
|
||||
- Location: `~/.pi/agent/keybindings.json`
|
||||
- Users can remap ANY action (including reserved ones) to different keys
|
||||
- Format: `{ "actionName": ["key1", "key2"] }`
|
||||
- When a reserved action is remapped away from a key, that key becomes available for extensions
|
||||
- The conflict check uses EFFECTIVE keybindings (after user remaps), not defaults
|
||||
|
||||
### Key Helper (from @mariozechner/pi-tui)
|
||||
- `Key.ctrl("x")` → `"ctrl+x"`
|
||||
- `Key.shift("tab")` → `"shift+tab"`
|
||||
- `Key.alt("left")` → `"alt+left"`
|
||||
- `Key.ctrlShift("p")` → `"ctrl+shift+p"`
|
||||
- `Key.ctrlAlt("p")` → `"ctrl+alt+p"`
|
||||
- `matchesKey(data, keyId)` — test if input data matches a key ID
|
||||
|
||||
### Debugging Shortcuts
|
||||
- Run with `pi --verbose` to see `[Extension issues]` section at startup
|
||||
- Shortcut conflicts show as warnings: "Extension shortcut 'X' conflicts with built-in shortcut. Skipping."
|
||||
- Extension shortcut errors appear as red text in the chat area
|
||||
- Shortcuts not matching in `matchesKey()` means the terminal isn't sending the expected escape sequence
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi keybindings documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/keybindings.md -f markdown -o /tmp/pi-keybindings-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/keybindings.md -o /tmp/pi-keybindings-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-keybindings-docs.md to have the freshest reference.
|
||||
|
||||
Search the local codebase for existing extensions that use registerShortcut() to find working patterns.
|
||||
|
||||
## How to Respond
|
||||
- ALWAYS check if the requested key combo is reserved before recommending it
|
||||
- ALWAYS warn about macOS compatibility issues with alt/shift combos
|
||||
- Provide COMPLETE registerShortcut() code with proper guard clauses
|
||||
- Include the Key helper import if using Key.ctrl() style
|
||||
- Recommend safe alternatives when a requested key is taken
|
||||
- Show how to debug with `--verbose` if shortcuts aren't firing
|
||||
- When suggesting keys, prefer this priority: free ctrl+letter > function keys > overridable non-reserved keys
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
name: pi-orchestrator
|
||||
description: Primary meta-agent that coordinates experts and builds Pi components
|
||||
tools: read,write,edit,bash,grep,find,ls,query_experts
|
||||
---
|
||||
You are **Pi Pi** — a meta-agent that builds Pi agents. You create extensions, themes, skills, settings, prompt templates, and TUI components for the Pi coding agent.
|
||||
|
||||
## Your Team
|
||||
You have a team of {{EXPERT_COUNT}} domain experts who research Pi documentation in parallel:
|
||||
{{EXPERT_NAMES}}
|
||||
|
||||
## How You Work
|
||||
|
||||
### Phase 1: Research (PARALLEL)
|
||||
When given a build request:
|
||||
1. Identify which domains are relevant
|
||||
2. Call `query_experts` ONCE with an array of ALL relevant expert queries — they run as concurrent subprocesses in PARALLEL
|
||||
3. Ask specific questions: "How do I register a custom tool with renderCall?" not "Tell me about extensions"
|
||||
4. Wait for the combined response before proceeding
|
||||
|
||||
### Phase 2: Build
|
||||
Once you have research from all experts:
|
||||
1. Synthesize the findings into a coherent implementation plan
|
||||
2. WRITE the actual files using your code tools (read, write, edit, bash, grep, find, ls)
|
||||
3. Create complete, working implementations — no stubs or TODOs
|
||||
4. Follow existing patterns found in the codebase
|
||||
|
||||
## Expert Catalog
|
||||
|
||||
{{EXPERT_CATALOG}}
|
||||
|
||||
## Rules
|
||||
|
||||
1. **ALWAYS query experts FIRST** before writing any Pi-specific code. You need fresh documentation.
|
||||
2. **Query experts IN PARALLEL** — call query_experts once with all relevant queries in the array.
|
||||
3. **Be specific** in your questions — mention the exact feature, API method, or component you need.
|
||||
4. **You write the code** — experts only research. They cannot modify files.
|
||||
5. **Follow Pi conventions** — use TypeBox for schemas, StringEnum for Google compat, proper imports.
|
||||
6. **Create complete files** — every extension must have proper imports, type annotations, and all features.
|
||||
7. **Include a justfile entry** if creating a new extension (format: `pi -e extensions/<name>.ts`).
|
||||
|
||||
## What You Can Build
|
||||
- **Extensions** (.ts files) — custom tools, event hooks, commands, UI components
|
||||
- **Themes** (.json files) — color schemes with all 51 tokens
|
||||
- **Skills** (SKILL.md directories) — capability packages with scripts
|
||||
- **Settings** (settings.json) — configuration files
|
||||
- **Prompt Templates** (.md files) — reusable prompts with arguments
|
||||
- **Agent Definitions** (.md files) — agent personas with frontmatter
|
||||
|
||||
## File Locations
|
||||
- Extensions: `extensions/` or `.pi/extensions/`
|
||||
- Themes: `.pi/themes/`
|
||||
- Skills: `.pi/skills/`
|
||||
- Settings: `.pi/settings.json`
|
||||
- Prompts: `.pi/prompts/`
|
||||
- Agents: `.pi/agents/`
|
||||
- Teams: `.pi/agents/teams.yaml`
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
name: prompt-expert
|
||||
description: Pi prompt templates expert — knows the single-file .md format, frontmatter, positional arguments ($1, $@, ${@:N}), discovery locations, and /template invocation
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are a prompt templates expert for the Pi coding agent. You know EVERYTHING about creating Pi prompt templates.
|
||||
|
||||
## Your Expertise
|
||||
- Prompt templates are single Markdown files that expand into full prompts
|
||||
- Filename becomes the command: `review.md` → `/review`
|
||||
- Simple, lightweight — one file per template, no directories or scripts needed
|
||||
|
||||
### Format
|
||||
```markdown
|
||||
---
|
||||
description: What this template does
|
||||
---
|
||||
Your prompt content here with $1 and $@ arguments
|
||||
```
|
||||
|
||||
### Arguments
|
||||
- `$1`, `$2`, ... — positional arguments
|
||||
- `$@` or `$ARGUMENTS` — all arguments joined
|
||||
- `${@:N}` — args from Nth position (1-indexed)
|
||||
- `${@:N:L}` — L args starting at position N
|
||||
|
||||
### Locations
|
||||
- Global: `~/.pi/agent/prompts/*.md`
|
||||
- Project: `.pi/prompts/*.md`
|
||||
- Packages: `prompts/` directories or `pi.prompts` entries in package.json
|
||||
- Settings: `prompts` array with files or directories
|
||||
- CLI: `--prompt-template <path>` (repeatable)
|
||||
|
||||
### Discovery
|
||||
- Non-recursive — only direct .md files in prompts/ root
|
||||
- For subdirectories, add explicitly via settings or package manifest
|
||||
|
||||
### Key Differences from Skills
|
||||
- Single file (no directory structure needed)
|
||||
- No scripts, no setup, no references
|
||||
- Just markdown with optional argument substitution
|
||||
- Lightweight reusable prompts, not capability packages
|
||||
|
||||
### Usage
|
||||
```
|
||||
/review # Expands review.md
|
||||
/component Button # Expands with argument
|
||||
/component Button "click handler" # Multiple arguments
|
||||
```
|
||||
|
||||
### Description
|
||||
- Optional frontmatter field
|
||||
- If missing, first non-empty line is used as description
|
||||
- Shown in autocomplete when typing `/`
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi prompt templates documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/prompt-templates.md -f markdown -o /tmp/pi-prompt-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/prompt-templates.md -o /tmp/pi-prompt-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-prompt-docs.md to have the freshest reference. Also search the local codebase (.pi/prompts/) for existing prompt template examples.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE .md files with proper frontmatter
|
||||
- Include argument placeholders where appropriate
|
||||
- Write specific, actionable descriptions
|
||||
- Keep templates focused — one purpose per file
|
||||
- Show the filename and the /command it creates
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: skill-expert
|
||||
description: Pi skills expert — knows SKILL.md format, frontmatter fields, directory structure, validation rules, and skill command registration
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are a skills expert for the Pi coding agent. You know EVERYTHING about creating Pi skills.
|
||||
|
||||
## Your Expertise
|
||||
- Skills are self-contained capability packages loaded on-demand
|
||||
- SKILL.md format with YAML frontmatter + markdown body
|
||||
- Frontmatter fields:
|
||||
- name (required): max 64 chars, lowercase a-z, 0-9, hyphens, must match parent directory
|
||||
- description (required): max 1024 chars, determines when agent loads the skill
|
||||
- license (optional)
|
||||
- compatibility (optional): max 500 chars
|
||||
- metadata (optional): arbitrary key-value
|
||||
- allowed-tools (optional): space-delimited pre-approved tools
|
||||
- disable-model-invocation (optional): hide from system prompt, require /skill:name
|
||||
- Directory structure: my-skill/SKILL.md + scripts/ + references/ + assets/
|
||||
- Skill locations: ~/.pi/agent/skills/, .pi/skills/, packages, settings.json
|
||||
- Discovery: direct .md files in root, recursive SKILL.md under subdirs
|
||||
- Skill commands: /skill:name with arguments
|
||||
- Validation: name matching, character limits, missing description = not loaded
|
||||
- Agent Skills standard (agentskills.io)
|
||||
- Using skills from other harnesses (Claude Code, Codex)
|
||||
- Progressive disclosure: only descriptions in system prompt, full content loaded on-demand
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi skills documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/skills.md -f markdown -o /tmp/pi-skill-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/skills.md -o /tmp/pi-skill-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-skill-docs.md to have the freshest reference. Also search the local codebase for existing skill examples.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE SKILL.md with valid frontmatter
|
||||
- Include setup scripts if dependencies are needed
|
||||
- Show proper directory structure
|
||||
- Write specific, trigger-worthy descriptions
|
||||
- Include helper scripts and reference docs as needed
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: theme-expert
|
||||
description: Pi themes expert — knows the JSON format, all 51 color tokens, vars system, hex/256-color values, hot reload, and theme distribution
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are a themes expert for the Pi coding agent. You know EVERYTHING about creating and distributing Pi themes.
|
||||
|
||||
## Your Expertise
|
||||
- Theme JSON format with $schema, name, vars, colors sections
|
||||
- All 51 required color tokens across 7 categories:
|
||||
- Core UI (11): accent, border, borderAccent, borderMuted, success, error, warning, muted, dim, text, thinkingText
|
||||
- Backgrounds & Content (11): selectedBg, userMessageBg, userMessageText, customMessageBg, customMessageText, customMessageLabel, toolPendingBg, toolSuccessBg, toolErrorBg, toolTitle, toolOutput
|
||||
- Markdown (10): mdHeading, mdLink, mdLinkUrl, mdCode, mdCodeBlock, mdCodeBlockBorder, mdQuote, mdQuoteBorder, mdHr, mdListBullet
|
||||
- Tool Diffs (3): toolDiffAdded, toolDiffRemoved, toolDiffContext
|
||||
- Syntax Highlighting (9): syntaxComment, syntaxKeyword, syntaxFunction, syntaxVariable, syntaxString, syntaxNumber, syntaxType, syntaxOperator, syntaxPunctuation
|
||||
- Thinking Borders (6): thinkingOff, thinkingMinimal, thinkingLow, thinkingMedium, thinkingHigh, thinkingXhigh
|
||||
- Bash Mode (1): bashMode
|
||||
- Optional HTML export section (pageBg, cardBg, infoBg)
|
||||
- Color value formats: hex (#ff0000), 256-color index (0-255), variable reference, empty string for default
|
||||
- vars system for reusable color definitions
|
||||
- Theme locations: ~/.pi/agent/themes/, .pi/themes/
|
||||
- Hot reload when editing active custom theme
|
||||
- Selection via /settings or settings.json
|
||||
- $schema URL for editor validation
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi themes documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/themes.md -f markdown -o /tmp/pi-theme-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/themes.md -o /tmp/pi-theme-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-theme-docs.md to have the freshest reference. Also search the local codebase (.pi/themes/) for existing theme examples.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE theme JSON with ALL 51 color tokens (no partial themes)
|
||||
- Use vars for palette consistency
|
||||
- Include the $schema for validation
|
||||
- Suggest color harmonies based on the user's aesthetic preference
|
||||
- Mention hot reload and testing tips
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
name: tui-expert
|
||||
description: Pi TUI expert — knows all built-in components (Text, Box, Container, Markdown, Image, SelectList, SettingsList, BorderedLoader), custom components, overlays, keyboard input, widgets, footers, and custom editors
|
||||
tools: read,grep,find,ls,bash
|
||||
---
|
||||
You are a TUI (Terminal User Interface) expert for the Pi coding agent. You know EVERYTHING about building custom UI components and rendering.
|
||||
|
||||
## Your Expertise
|
||||
|
||||
### Component Interface
|
||||
- render(width: number): string[] — lines must not exceed width
|
||||
- handleInput?(data: string) — keyboard input when focused
|
||||
- wantsKeyRelease? — for Kitty protocol key release events
|
||||
- invalidate() — clear cached render state
|
||||
|
||||
### Built-in Components (from @mariozechner/pi-tui)
|
||||
- Text: multi-line text with word wrapping, paddingX, paddingY, background function
|
||||
- Box: container with padding and background color
|
||||
- Container: groups children vertically, addChild/removeChild
|
||||
- Spacer: empty vertical space
|
||||
- Markdown: renders markdown with syntax highlighting
|
||||
- Image: renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm)
|
||||
- SelectList: selection dialog with theme, onSelect/onCancel
|
||||
- SettingsList: toggle settings with theme
|
||||
|
||||
### From @mariozechner/pi-coding-agent
|
||||
- DynamicBorder: border with color function — ALWAYS type the param: (s: string) => theme.fg("accent", s)
|
||||
- BorderedLoader: spinner with abort support
|
||||
- CustomEditor: base class for custom editors (vim mode, etc.)
|
||||
|
||||
### Keyboard Input
|
||||
- matchesKey(data, Key.up/down/enter/escape/etc.)
|
||||
- Key modifiers: Key.ctrl("c"), Key.shift("tab"), Key.alt("left"), Key.ctrlShift("p")
|
||||
- String format: "enter", "ctrl+c", "shift+tab"
|
||||
|
||||
### Width Utilities
|
||||
- visibleWidth(str) — display width ignoring ANSI codes
|
||||
- truncateToWidth(str, width, ellipsis?) — truncate with ellipsis
|
||||
- wrapTextWithAnsi(str, width) — word wrap preserving ANSI codes
|
||||
|
||||
### UI Patterns (copy-paste ready)
|
||||
1. Selection Dialog: SelectList + DynamicBorder + ctx.ui.custom()
|
||||
2. Async with Cancel: BorderedLoader with signal
|
||||
3. Settings/Toggles: SettingsList + getSettingsListTheme()
|
||||
4. Status Indicator: ctx.ui.setStatus(key, styledText)
|
||||
5. Widgets: ctx.ui.setWidget(key, lines | factory, { placement })
|
||||
6. Custom Footer: ctx.ui.setFooter(factory)
|
||||
7. Custom Editor: extend CustomEditor, ctx.ui.setEditorComponent(factory)
|
||||
8. Overlays: ctx.ui.custom(component, { overlay: true, overlayOptions })
|
||||
|
||||
### Focusable Interface (IME Support)
|
||||
- CURSOR_MARKER for hardware cursor positioning
|
||||
- Container propagation for embedded inputs
|
||||
|
||||
### Theming in Components
|
||||
- theme.fg(color, text) for foreground
|
||||
- theme.bg(color, text) for background
|
||||
- theme.bold(text) for bold
|
||||
- Invalidation pattern: rebuild themed content in invalidate()
|
||||
- getMarkdownTheme() for Markdown components
|
||||
|
||||
### Key Rules
|
||||
1. Always use theme from callback — not imported directly
|
||||
2. Always type DynamicBorder color param: (s: string) =>
|
||||
3. Call tui.requestRender() after state changes in handleInput
|
||||
4. Return { render, invalidate, handleInput } for custom components
|
||||
5. Use Text with padding (0, 0) — Box handles padding
|
||||
6. Cache rendered output with cachedWidth/cachedLines pattern
|
||||
|
||||
## CRITICAL: First Action
|
||||
Before answering ANY question, you MUST fetch the latest Pi TUI documentation:
|
||||
|
||||
```bash
|
||||
firecrawl scrape https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/tui.md -f markdown -o /tmp/pi-tui-docs.md || curl -sL https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/tui.md -o /tmp/pi-tui-docs.md
|
||||
```
|
||||
|
||||
Then read /tmp/pi-tui-docs.md to have the freshest reference. Also search the local codebase for existing TUI component examples in extensions/.
|
||||
|
||||
## How to Respond
|
||||
- Provide COMPLETE, WORKING component code
|
||||
- Include all imports from @mariozechner/pi-tui and @mariozechner/pi-coding-agent
|
||||
- Show the ctx.ui.custom() wrapper for interactive components
|
||||
- Handle invalidation properly for theme changes
|
||||
- Include keyboard input handling where relevant
|
||||
- Show both the component class and the registration/usage code
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: plan-reviewer
|
||||
description: Plan critic — reviews, challenges, and validates implementation plans
|
||||
tools: read,grep,find,ls
|
||||
---
|
||||
You are a plan reviewer agent. Your job is to critically evaluate implementation plans.
|
||||
|
||||
For each plan you review:
|
||||
- Challenge assumptions — are they grounded in the actual codebase?
|
||||
- Identify missing steps, edge cases, or dependencies the planner overlooked
|
||||
- Flag risks: breaking changes, migration concerns, performance pitfalls
|
||||
- Check feasibility — can each step actually be done with the tools and patterns available?
|
||||
- Evaluate ordering — are steps in the right sequence? Are there hidden dependencies?
|
||||
- Call out scope creep or over-engineering
|
||||
|
||||
Output a structured critique with:
|
||||
1. **Strengths** — what the plan gets right
|
||||
2. **Issues** — concrete problems ranked by severity
|
||||
3. **Missing** — steps or considerations the plan omitted
|
||||
4. **Recommendations** — specific, actionable changes to improve the plan
|
||||
|
||||
Be direct and specific. Reference actual files and patterns from the codebase when possible. Do NOT modify files.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: planner
|
||||
description: Architecture and implementation planning
|
||||
tools: read,grep,find,ls
|
||||
---
|
||||
You are a planner agent. Analyze requirements and produce clear, actionable implementation plans. Identify files to change, dependencies, and risks. Output a numbered step-by-step plan. Do NOT modify files.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: red-team
|
||||
description: Security and adversarial testing
|
||||
tools: read,bash,grep,find,ls
|
||||
---
|
||||
You are a red team agent. Find security vulnerabilities, edge cases, and failure modes. Check for injection risks, exposed secrets, missing validation, and unsafe defaults. Report findings with severity ratings. Do NOT modify files.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: Code review and quality checks
|
||||
tools: read,bash,grep,find,ls
|
||||
---
|
||||
You are a code reviewer agent. Review code for bugs, security issues, style problems, and improvements. Run tests if available. Be concise and use bullet points. Do NOT modify files.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
name: scout
|
||||
description: Fast recon and codebase exploration
|
||||
tools: read,grep,find,ls
|
||||
---
|
||||
You are a scout agent. Investigate the codebase quickly and report findings concisely. Do NOT modify any files. Focus on structure, patterns, and key entry points.
|
||||
@@ -1,31 +0,0 @@
|
||||
full:
|
||||
- scout
|
||||
- planner
|
||||
- builder
|
||||
- reviewer
|
||||
- documenter
|
||||
- red-team
|
||||
|
||||
plan-build:
|
||||
- planner
|
||||
- builder
|
||||
- reviewer
|
||||
|
||||
info:
|
||||
- scout
|
||||
- documenter
|
||||
- reviewer
|
||||
|
||||
frontend:
|
||||
- planner
|
||||
- builder
|
||||
- bowser
|
||||
|
||||
pi-pi:
|
||||
- ext-expert
|
||||
- theme-expert
|
||||
- skill-expert
|
||||
- config-expert
|
||||
- tui-expert
|
||||
- prompt-expert
|
||||
- agent-expert
|
||||
@@ -1,279 +0,0 @@
|
||||
bashToolPatterns:
|
||||
- pattern: '\brm\s+(-[^\s]*)*-[rRf]'
|
||||
reason: rm with recursive or force flags
|
||||
- pattern: '\brm\s+-[rRf]'
|
||||
reason: rm with recursive or force flags
|
||||
- pattern: '\brm\s+--recursive'
|
||||
reason: rm with --recursive flag
|
||||
- pattern: '\brm\s+--force'
|
||||
reason: rm with --force flag
|
||||
- pattern: '\bsudo\s+rm\b'
|
||||
reason: sudo rm
|
||||
- pattern: '\brmdir\s+--ignore-fail-on-non-empty'
|
||||
reason: rmdir ignore-fail
|
||||
- pattern: '\bchmod\s+(-[^\s]+\s+)*777\b'
|
||||
reason: chmod 777 (world writable)
|
||||
- pattern: '\bchmod\s+-[Rr].*777'
|
||||
reason: recursive chmod 777
|
||||
- pattern: '\bchown\s+-[Rr].*\broot\b'
|
||||
reason: recursive chown to root
|
||||
- pattern: '\bgit\s+reset\s+--hard\b'
|
||||
reason: git reset --hard (use --soft or stash)
|
||||
- pattern: '\bgit\s+clean\s+(-[^\s]*)*-[fd]'
|
||||
reason: git clean with force/directory flags
|
||||
- pattern: '\bgit\s+push\s+.*--force(?!-with-lease)'
|
||||
reason: git push --force (use --force-with-lease)
|
||||
- pattern: '\bgit\s+push\s+(-[^\s]*)*-f\b'
|
||||
reason: git push -f (use --force-with-lease)
|
||||
- pattern: '\bgit\s+stash\s+clear\b'
|
||||
reason: git stash clear (deletes ALL stashes)
|
||||
- pattern: '\bgit\s+reflog\s+expire\b'
|
||||
reason: git reflog expire (destroys recovery mechanism)
|
||||
- pattern: '\bgit\s+gc\s+.*--prune=now'
|
||||
reason: git gc --prune=now (can lose dangling commits)
|
||||
- pattern: '\bgit\s+filter-branch\b'
|
||||
reason: git filter-branch (rewrites entire history)
|
||||
- pattern: '\bgit\s+checkout\s+--\s*\.'
|
||||
reason: Discards all uncommitted changes
|
||||
ask: true
|
||||
- pattern: '\bgit\s+restore\s+\.'
|
||||
reason: Discards all uncommitted changes
|
||||
ask: true
|
||||
- pattern: '\bgit\s+stash\s+drop\b'
|
||||
reason: Permanently deletes a stash
|
||||
ask: true
|
||||
- pattern: '\bgit\s+branch\s+(-[^\s]*)*-D'
|
||||
reason: Force deletes branch (even if unmerged)
|
||||
ask: true
|
||||
- pattern: '\bgit\s+push\s+\S+\s+--delete\b'
|
||||
reason: Deletes remote branch
|
||||
ask: true
|
||||
- pattern: '\bgit\s+push\s+\S+\s+:\S+'
|
||||
reason: Deletes remote branch (old syntax)
|
||||
ask: true
|
||||
- pattern: '\bmkfs\.'
|
||||
reason: filesystem format command
|
||||
- pattern: '\bdd\s+.*of=/dev/'
|
||||
reason: dd writing to device
|
||||
- pattern: '\bkill\s+-9\s+-1\b'
|
||||
reason: kill all processes
|
||||
- pattern: '\bkillall\s+-9\b'
|
||||
reason: killall -9
|
||||
- pattern: '\bpkill\s+-9\b'
|
||||
reason: pkill -9
|
||||
- pattern: '\bhistory\s+-c\b'
|
||||
reason: clearing shell history
|
||||
- pattern: '\baws\s+s3\s+rm\s+.*--recursive'
|
||||
reason: aws s3 rm --recursive (deletes all objects)
|
||||
- pattern: '\baws\s+s3\s+rb\s+.*--force'
|
||||
reason: aws s3 rb --force (force removes bucket)
|
||||
- pattern: '\baws\s+ec2\s+terminate-instances\b'
|
||||
reason: aws ec2 terminate-instances
|
||||
- pattern: '\baws\s+rds\s+delete-db-instance\b'
|
||||
reason: aws rds delete-db-instance
|
||||
- pattern: '\baws\s+cloudformation\s+delete-stack\b'
|
||||
reason: aws cloudformation delete-stack (deletes infrastructure)
|
||||
- pattern: '\baws\s+dynamodb\s+delete-table\b'
|
||||
reason: aws dynamodb delete-table
|
||||
- pattern: '\baws\s+eks\s+delete-cluster\b'
|
||||
reason: aws eks delete-cluster
|
||||
- pattern: '\baws\s+lambda\s+delete-function\b'
|
||||
reason: aws lambda delete-function
|
||||
- pattern: '\baws\s+iam\s+delete-role\b'
|
||||
reason: aws iam delete-role
|
||||
- pattern: '\baws\s+iam\s+delete-user\b'
|
||||
reason: aws iam delete-user
|
||||
- pattern: '\bgcloud\s+projects\s+delete\b'
|
||||
reason: gcloud projects delete (DELETES ENTIRE PROJECT)
|
||||
- pattern: '\bgcloud\s+compute\s+instances\s+delete\b'
|
||||
reason: gcloud compute instances delete
|
||||
- pattern: '\bgcloud\s+sql\s+instances\s+delete\b'
|
||||
reason: gcloud sql instances delete
|
||||
- pattern: '\bgcloud\s+container\s+clusters\s+delete\b'
|
||||
reason: gcloud container clusters delete (GKE)
|
||||
- pattern: '\bgcloud\s+storage\s+rm\s+.*-r'
|
||||
reason: gcloud storage rm -r (recursive delete)
|
||||
- pattern: '\bgcloud\s+functions\s+delete\b'
|
||||
reason: gcloud functions delete
|
||||
- pattern: '\bgcloud\s+iam\s+service-accounts\s+delete\b'
|
||||
reason: gcloud iam service-accounts delete
|
||||
- pattern: '\bgcloud\s+run\s+services\s+delete\b'
|
||||
reason: gcloud run services delete (deletes Cloud Run service)
|
||||
- pattern: '\bgcloud\s+run\s+jobs\s+delete\b'
|
||||
reason: gcloud run jobs delete (deletes Cloud Run job)
|
||||
- pattern: '\bgcloud\s+services\s+disable\b'
|
||||
reason: gcloud services disable (disables GCP APIs)
|
||||
- pattern: '\bgcloud\s+iam\s+roles\s+delete\b'
|
||||
reason: gcloud iam roles delete (deletes IAM role)
|
||||
- pattern: '\bgcloud\s+iam\s+policies\b'
|
||||
reason: gcloud iam policies (modifies IAM policies)
|
||||
ask: true
|
||||
- pattern: '\bfirebase\s+projects:delete\b'
|
||||
reason: firebase projects:delete (deletes entire project)
|
||||
- pattern: '\bfirebase\s+firestore:delete\s+.*--all-collections'
|
||||
reason: firebase firestore:delete --all-collections (wipes all data)
|
||||
- pattern: '\bfirebase\s+database:remove\b'
|
||||
reason: firebase database:remove (wipes Realtime DB)
|
||||
- pattern: '\bfirebase\s+hosting:disable\b'
|
||||
reason: firebase hosting:disable
|
||||
- pattern: '\bfirebase\s+functions:delete\b'
|
||||
reason: firebase functions:delete
|
||||
- pattern: '\bvercel\s+remove\s+.*--yes'
|
||||
reason: vercel remove --yes (removes deployment)
|
||||
- pattern: '\bvercel\s+projects\s+rm\b'
|
||||
reason: vercel projects rm (deletes project)
|
||||
- pattern: '\bvercel\s+env\s+rm\b'
|
||||
reason: vercel env rm (removes env variables)
|
||||
- pattern: '\bvercel\s+rm\b'
|
||||
reason: vercel rm (removes deployment)
|
||||
- pattern: '\bvercel\s+remove\b'
|
||||
reason: vercel remove (removes deployment)
|
||||
- pattern: '\bvercel\s+domains\s+rm\b'
|
||||
reason: vercel domains rm (removes custom domain)
|
||||
- pattern: '\bnetlify\s+sites:delete\b'
|
||||
reason: netlify sites:delete (deletes entire site)
|
||||
- pattern: '\bnetlify\s+functions:delete\b'
|
||||
reason: netlify functions:delete
|
||||
- pattern: '\bwrangler\s+delete\b'
|
||||
reason: wrangler delete (deletes Worker)
|
||||
- pattern: '\bwrangler\s+r2\s+bucket\s+delete\b'
|
||||
reason: wrangler r2 bucket delete
|
||||
- pattern: '\bwrangler\s+kv:namespace\s+delete\b'
|
||||
reason: wrangler kv:namespace delete
|
||||
- pattern: '\bwrangler\s+d1\s+delete\b'
|
||||
reason: wrangler d1 delete (deletes database)
|
||||
- pattern: '\bwrangler\s+queues\s+delete\b'
|
||||
reason: wrangler queues delete
|
||||
- pattern: 'DELETE\s+FROM\s+\w+\s*;'
|
||||
reason: DELETE without WHERE clause (will delete ALL rows)
|
||||
- pattern: 'DELETE\s+\*\s+FROM'
|
||||
reason: DELETE * (will delete ALL rows)
|
||||
- pattern: '\bTRUNCATE\s+TABLE\b'
|
||||
reason: TRUNCATE TABLE (will delete ALL rows)
|
||||
- pattern: '\bDROP\s+TABLE\b'
|
||||
reason: DROP TABLE
|
||||
- pattern: '\bDROP\s+DATABASE\b'
|
||||
reason: DROP DATABASE
|
||||
- pattern: '\bDROP\s+SCHEMA\b'
|
||||
reason: DROP SCHEMA
|
||||
- pattern: '\bDELETE\s+FROM\s+\w+\s+WHERE\b.*\bid\s*='
|
||||
reason: SQL DELETE with specific ID
|
||||
ask: true
|
||||
|
||||
zeroAccessPaths:
|
||||
- ".env"
|
||||
- ".env.local"
|
||||
- ".env.development"
|
||||
- ".env.production"
|
||||
- ".env.staging"
|
||||
- ".env.test"
|
||||
- ".env.*.local"
|
||||
- "*.env"
|
||||
- "~/.ssh/"
|
||||
- "~/.gnupg/"
|
||||
- "~/.aws/"
|
||||
- "~/.config/gcloud/"
|
||||
- "*-credentials.json"
|
||||
- "*serviceAccount*.json"
|
||||
- "*service-account*.json"
|
||||
- "~/.azure/"
|
||||
- "~/.kube/"
|
||||
- "kubeconfig"
|
||||
- "*-secret.yaml"
|
||||
- "secrets.yaml"
|
||||
- "~/.docker/"
|
||||
- "*.pem"
|
||||
- "*.key"
|
||||
- "*.p12"
|
||||
- "*.pfx"
|
||||
- "*.tfstate"
|
||||
- "*.tfstate.backup"
|
||||
- ".terraform/"
|
||||
- ".vercel/"
|
||||
- ".netlify/"
|
||||
- "firebase-adminsdk*.json"
|
||||
- "serviceAccountKey.json"
|
||||
- ".supabase/"
|
||||
- "~/.netrc"
|
||||
- "~/.npmrc"
|
||||
- "~/.pypirc"
|
||||
- "~/.git-credentials"
|
||||
- ".git-credentials"
|
||||
- "dump.sql"
|
||||
- "backup.sql"
|
||||
- "*.dump"
|
||||
|
||||
readOnlyPaths:
|
||||
- /etc/
|
||||
- /usr/
|
||||
- /bin/
|
||||
- /sbin/
|
||||
- /boot/
|
||||
- /root/
|
||||
- ~/.bash_history
|
||||
- ~/.zsh_history
|
||||
- ~/.node_repl_history
|
||||
- ~/.bashrc
|
||||
- ~/.zshrc
|
||||
- ~/.profile
|
||||
- ~/.bash_profile
|
||||
- "package-lock.json"
|
||||
- "yarn.lock"
|
||||
- "pnpm-lock.yaml"
|
||||
- "Gemfile.lock"
|
||||
- "poetry.lock"
|
||||
- "Pipfile.lock"
|
||||
- "composer.lock"
|
||||
- "Cargo.lock"
|
||||
- "go.sum"
|
||||
- "flake.lock"
|
||||
- "bun.lockb"
|
||||
- "uv.lock"
|
||||
- "npm-shrinkwrap.json"
|
||||
- "*.lock"
|
||||
- "*.lockb"
|
||||
- "*.min.js"
|
||||
- "*.min.css"
|
||||
- "*.bundle.js"
|
||||
- "*.chunk.js"
|
||||
- dist/
|
||||
- build/
|
||||
- .next/
|
||||
- .nuxt/
|
||||
- .output/
|
||||
- node_modules/
|
||||
- __pycache__/
|
||||
- .venv/
|
||||
- venv/
|
||||
- target/
|
||||
|
||||
noDeletePaths:
|
||||
- ~/.claude/
|
||||
- CLAUDE.md
|
||||
- "LICENSE"
|
||||
- "LICENSE.*"
|
||||
- "COPYING"
|
||||
- "COPYING.*"
|
||||
- "NOTICE"
|
||||
- "PATENTS"
|
||||
- "README.md"
|
||||
- "README.*"
|
||||
- "CONTRIBUTING.md"
|
||||
- "CHANGELOG.md"
|
||||
- "CODE_OF_CONDUCT.md"
|
||||
- "SECURITY.md"
|
||||
- .git/
|
||||
- .gitignore
|
||||
- .gitattributes
|
||||
- .gitmodules
|
||||
- .github/
|
||||
- .gitlab-ci.yml
|
||||
- .circleci/
|
||||
- Jenkinsfile
|
||||
- .travis.yml
|
||||
- azure-pipelines.yml
|
||||
- Dockerfile
|
||||
- "Dockerfile.*"
|
||||
- docker-compose.yml
|
||||
- "docker-compose.*.yml"
|
||||
- .dockerignore
|
||||
@@ -1,719 +0,0 @@
|
||||
/**
|
||||
* Calvana Ship Log Extension
|
||||
*
|
||||
* Automatically tracks what you're shipping and updates the live Calvana site.
|
||||
*
|
||||
* Tools (LLM-callable):
|
||||
* - calvana_ship: Add/update/complete shipping log entries
|
||||
* - calvana_oops: Log mistakes and fixes
|
||||
* - calvana_deploy: Push changes to the live site
|
||||
*
|
||||
* Commands (user):
|
||||
* /ships — View current shipping log
|
||||
* /ship-deploy — Force deploy to calvana.quikcue.com
|
||||
*
|
||||
* How it works:
|
||||
* 1. When you work on tasks, the LLM uses calvana_ship to track progress
|
||||
* 2. If something breaks, calvana_oops logs it
|
||||
* 3. calvana_deploy rebuilds the /live page HTML and pushes it to the server
|
||||
* 4. The extension auto-injects context so the LLM knows to track ships
|
||||
*
|
||||
* Edit the SSH/deploy config in the DEPLOY_CONFIG section below.
|
||||
*/
|
||||
|
||||
import { StringEnum } from "@mariozechner/pi-ai";
|
||||
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
||||
import { Text, truncateToWidth, matchesKey } from "@mariozechner/pi-tui";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION — Edit these to change deploy target, copy, links
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
const DEPLOY_CONFIG = {
|
||||
sshHost: "root@159.195.60.33",
|
||||
sshPort: "22",
|
||||
container: "qc-server-new",
|
||||
sitePath: "/opt/calvana/html",
|
||||
domain: "calvana.quikcue.com",
|
||||
};
|
||||
|
||||
const SITE_CONFIG = {
|
||||
title: "Calvana",
|
||||
tagline: "I break rules. Not production.",
|
||||
email: "omair@quikcue.com",
|
||||
referralLine: "PS — Umar pointed me here. If this turns into a hire, I want him to get paid.",
|
||||
};
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TYPES
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
type ShipStatus = "planned" | "shipping" | "shipped";
|
||||
|
||||
interface ShipEntry {
|
||||
id: number;
|
||||
title: string;
|
||||
status: ShipStatus;
|
||||
timestamp: string;
|
||||
metric: string;
|
||||
prLink: string;
|
||||
deployLink: string;
|
||||
loomLink: string;
|
||||
}
|
||||
|
||||
interface OopsEntry {
|
||||
id: number;
|
||||
description: string;
|
||||
fixTime: string;
|
||||
commitLink: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface ShipLogState {
|
||||
ships: ShipEntry[];
|
||||
oops: OopsEntry[];
|
||||
nextShipId: number;
|
||||
nextOopsId: number;
|
||||
lastDeployed: string | null;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL SCHEMAS
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
const ShipParams = Type.Object({
|
||||
action: StringEnum(["add", "update", "list"] as const),
|
||||
title: Type.Optional(Type.String({ description: "Ship title (for add)" })),
|
||||
id: Type.Optional(Type.Number({ description: "Ship ID (for update)" })),
|
||||
status: Type.Optional(StringEnum(["planned", "shipping", "shipped"] as const)),
|
||||
metric: Type.Optional(Type.String({ description: "What moved — metric line" })),
|
||||
prLink: Type.Optional(Type.String({ description: "PR link" })),
|
||||
deployLink: Type.Optional(Type.String({ description: "Deploy link" })),
|
||||
loomLink: Type.Optional(Type.String({ description: "Loom clip link" })),
|
||||
});
|
||||
|
||||
const OopsParams = Type.Object({
|
||||
action: StringEnum(["add", "list"] as const),
|
||||
description: Type.Optional(Type.String({ description: "What broke and how it was fixed" })),
|
||||
fixTime: Type.Optional(Type.String({ description: "Time to fix, e.g. '3 min'" })),
|
||||
commitLink: Type.Optional(Type.String({ description: "Link to the fix commit" })),
|
||||
});
|
||||
|
||||
const DeployParams = Type.Object({
|
||||
dryRun: Type.Optional(Type.Boolean({ description: "If true, generate HTML but don't deploy" })),
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// EXTENSION
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// ── State ──
|
||||
let state: ShipLogState = {
|
||||
ships: [],
|
||||
oops: [],
|
||||
nextShipId: 1,
|
||||
nextOopsId: 1,
|
||||
lastDeployed: null,
|
||||
};
|
||||
|
||||
// ── State reconstruction from session ──
|
||||
const reconstructState = (ctx: ExtensionContext) => {
|
||||
state = { ships: [], oops: [], nextShipId: 1, nextOopsId: 1, lastDeployed: null };
|
||||
|
||||
for (const entry of ctx.sessionManager.getBranch()) {
|
||||
if (entry.type !== "message") continue;
|
||||
const msg = entry.message;
|
||||
if (msg.role !== "toolResult") continue;
|
||||
if (msg.toolName === "calvana_ship" || msg.toolName === "calvana_oops" || msg.toolName === "calvana_deploy") {
|
||||
const details = msg.details as { state?: ShipLogState } | undefined;
|
||||
if (details?.state) {
|
||||
state = details.state;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
reconstructState(ctx);
|
||||
if (ctx.hasUI) {
|
||||
const theme = ctx.ui.theme;
|
||||
const shipCount = state.ships.length;
|
||||
const shipped = state.ships.filter(s => s.status === "shipped").length;
|
||||
ctx.ui.setStatus("calvana", theme.fg("dim", `🚀 ${shipped}/${shipCount} shipped`));
|
||||
}
|
||||
});
|
||||
pi.on("session_switch", async (_event, ctx) => reconstructState(ctx));
|
||||
pi.on("session_fork", async (_event, ctx) => reconstructState(ctx));
|
||||
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
|
||||
|
||||
// ── Inject context so LLM knows about ship tracking ──
|
||||
pi.on("before_agent_start", async (event, _ctx) => {
|
||||
const shipContext = `
|
||||
[Calvana Ship Log Extension Active]
|
||||
You have access to these tools for tracking work:
|
||||
- calvana_ship: Track shipping progress (add/update/list entries)
|
||||
- calvana_oops: Log mistakes and fixes
|
||||
- calvana_deploy: Push updates to the live site at https://${DEPLOY_CONFIG.domain}/live
|
||||
|
||||
When you START working on a task, use calvana_ship to add or update it to "shipping".
|
||||
When you COMPLETE a task, update it to "shipped" with a metric.
|
||||
If something BREAKS, log it with calvana_oops.
|
||||
After significant changes, use calvana_deploy to push updates live.
|
||||
|
||||
Current ships: ${state.ships.length} (${state.ships.filter(s => s.status === "shipped").length} shipped)
|
||||
Current oops: ${state.oops.length}
|
||||
`;
|
||||
return {
|
||||
systemPrompt: event.systemPrompt + shipContext,
|
||||
};
|
||||
});
|
||||
|
||||
// ── Update status bar on turn end ──
|
||||
pi.on("turn_end", async (_event, ctx) => {
|
||||
if (ctx.hasUI) {
|
||||
const theme = ctx.ui.theme;
|
||||
const shipped = state.ships.filter(s => s.status === "shipped").length;
|
||||
const shipping = state.ships.filter(s => s.status === "shipping").length;
|
||||
const total = state.ships.length;
|
||||
let statusText = `🚀 ${shipped}/${total} shipped`;
|
||||
if (shipping > 0) statusText += ` · ${shipping} in flight`;
|
||||
if (state.lastDeployed) statusText += ` · last deploy ${state.lastDeployed}`;
|
||||
ctx.ui.setStatus("calvana", theme.fg("dim", statusText));
|
||||
}
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// TOOL: calvana_ship
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
pi.registerTool({
|
||||
name: "calvana_ship",
|
||||
label: "Ship Log",
|
||||
description: "Track shipping progress. Actions: add (new entry), update (change status/links), list (show all). Use this whenever you start, progress, or finish a task.",
|
||||
parameters: ShipParams,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " GMT+8";
|
||||
|
||||
switch (params.action) {
|
||||
case "add": {
|
||||
if (!params.title) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: title required" }],
|
||||
details: { state: { ...state }, error: "title required" },
|
||||
};
|
||||
}
|
||||
const entry: ShipEntry = {
|
||||
id: state.nextShipId++,
|
||||
title: params.title,
|
||||
status: (params.status as ShipStatus) || "planned",
|
||||
timestamp: now,
|
||||
metric: params.metric || "—",
|
||||
prLink: params.prLink || "#pr",
|
||||
deployLink: params.deployLink || "#deploy",
|
||||
loomLink: params.loomLink || "#loomclip",
|
||||
};
|
||||
state.ships.push(entry);
|
||||
return {
|
||||
content: [{ type: "text", text: `Ship #${entry.id} added: "${entry.title}" [${entry.status}]` }],
|
||||
details: { state: { ...state, ships: [...state.ships] } },
|
||||
};
|
||||
}
|
||||
|
||||
case "update": {
|
||||
if (params.id === undefined) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: id required for update" }],
|
||||
details: { state: { ...state }, error: "id required" },
|
||||
};
|
||||
}
|
||||
const ship = state.ships.find(s => s.id === params.id);
|
||||
if (!ship) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Ship #${params.id} not found` }],
|
||||
details: { state: { ...state }, error: `#${params.id} not found` },
|
||||
};
|
||||
}
|
||||
if (params.status) ship.status = params.status as ShipStatus;
|
||||
if (params.metric) ship.metric = params.metric;
|
||||
if (params.prLink) ship.prLink = params.prLink;
|
||||
if (params.deployLink) ship.deployLink = params.deployLink;
|
||||
if (params.loomLink) ship.loomLink = params.loomLink;
|
||||
ship.timestamp = now;
|
||||
return {
|
||||
content: [{ type: "text", text: `Ship #${ship.id} updated: "${ship.title}" [${ship.status}]` }],
|
||||
details: { state: { ...state, ships: [...state.ships] } },
|
||||
};
|
||||
}
|
||||
|
||||
case "list": {
|
||||
if (state.ships.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No ships logged yet." }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
const lines = state.ships.map(s =>
|
||||
`#${s.id} [${s.status.toUpperCase()}] ${s.title} (${s.timestamp}) — ${s.metric}`
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("🚀 ship "));
|
||||
text += theme.fg("muted", args.action || "");
|
||||
if (args.title) text += " " + theme.fg("dim", `"${args.title}"`);
|
||||
if (args.id !== undefined) text += " " + theme.fg("accent", `#${args.id}`);
|
||||
if (args.status) text += " → " + theme.fg("accent", args.status);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded }, theme) {
|
||||
const details = result.details as { state?: ShipLogState; error?: string } | undefined;
|
||||
if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
||||
|
||||
const st = details?.state;
|
||||
if (!st || st.ships.length === 0) return new Text(theme.fg("dim", "No ships"), 0, 0);
|
||||
|
||||
const shipped = st.ships.filter(s => s.status === "shipped").length;
|
||||
const total = st.ships.length;
|
||||
let text = theme.fg("success", `${shipped}/${total} shipped`);
|
||||
|
||||
if (expanded) {
|
||||
for (const s of st.ships) {
|
||||
const badge = s.status === "shipped" ? theme.fg("success", "✓")
|
||||
: s.status === "shipping" ? theme.fg("warning", "●")
|
||||
: theme.fg("dim", "○");
|
||||
text += `\n ${badge} ${theme.fg("accent", `#${s.id}`)} ${theme.fg("muted", s.title)}`;
|
||||
}
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// TOOL: calvana_oops
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
pi.registerTool({
|
||||
name: "calvana_oops",
|
||||
label: "Oops Log",
|
||||
description: "Log mistakes and fixes. Actions: add (new oops entry), list (show all). Use when something breaks during a task.",
|
||||
parameters: OopsParams,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " GMT+8";
|
||||
|
||||
switch (params.action) {
|
||||
case "add": {
|
||||
if (!params.description) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: description required" }],
|
||||
details: { state: { ...state }, error: "description required" },
|
||||
};
|
||||
}
|
||||
const entry: OopsEntry = {
|
||||
id: state.nextOopsId++,
|
||||
description: params.description,
|
||||
fixTime: params.fixTime || "—",
|
||||
commitLink: params.commitLink || "#commit",
|
||||
timestamp: now,
|
||||
};
|
||||
state.oops.push(entry);
|
||||
return {
|
||||
content: [{ type: "text", text: `Oops #${entry.id}: "${entry.description}" (fixed in ${entry.fixTime})` }],
|
||||
details: { state: { ...state, oops: [...state.oops] } },
|
||||
};
|
||||
}
|
||||
|
||||
case "list": {
|
||||
if (state.oops.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No oops entries. Clean run so far." }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
const lines = state.oops.map(o =>
|
||||
`#${o.id} ${o.description} — fixed in ${o.fixTime}`
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("💥 oops "));
|
||||
text += theme.fg("muted", args.action || "");
|
||||
if (args.description) text += " " + theme.fg("dim", `"${args.description}"`);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, _options, theme) {
|
||||
const details = result.details as { state?: ShipLogState; error?: string } | undefined;
|
||||
if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
||||
const text = result.content[0];
|
||||
return new Text(theme.fg("warning", text?.type === "text" ? text.text : ""), 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// TOOL: calvana_deploy
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
pi.registerTool({
|
||||
name: "calvana_deploy",
|
||||
label: "Deploy Calvana",
|
||||
description: `Regenerate the /live page with current ship log and deploy to https://${DEPLOY_CONFIG.domain}. Call this after adding/updating ships or oops entries to push changes live.`,
|
||||
parameters: DeployParams,
|
||||
|
||||
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
||||
onUpdate?.({ content: [{ type: "text", text: "Generating HTML..." }] });
|
||||
|
||||
const liveHtml = generateLivePageHtml(state);
|
||||
|
||||
if (params.dryRun) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Dry run — generated ${liveHtml.length} bytes of HTML.\n\n${liveHtml.slice(0, 500)}...` }],
|
||||
details: { state: { ...state }, dryRun: true },
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate?.({ content: [{ type: "text", text: "Deploying to server..." }] });
|
||||
|
||||
try {
|
||||
// Write HTML to server via SSH + incus exec
|
||||
const escapedHtml = liveHtml.replace(/'/g, "'\\''");
|
||||
const sshCmd = `ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost}`;
|
||||
const writeCmd = `${sshCmd} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'cat > ${DEPLOY_CONFIG.sitePath}/live/index.html << '\\''HTMLEOF'\\''
|
||||
${liveHtml}
|
||||
HTMLEOF
|
||||
'"`;
|
||||
|
||||
// Use base64 to avoid all escaping nightmares
|
||||
const b64Html = Buffer.from(liveHtml).toString("base64");
|
||||
const deployResult = await pi.exec("bash", ["-c",
|
||||
`ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'echo ${b64Html} | base64 -d > ${DEPLOY_CONFIG.sitePath}/live/index.html'"`
|
||||
], { signal, timeout: 30000 });
|
||||
|
||||
if (deployResult.code !== 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Deploy failed: ${deployResult.stderr}` }],
|
||||
details: { state: { ...state }, error: deployResult.stderr },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Rebuild and update docker service
|
||||
const rebuildResult = await pi.exec("bash", ["-c",
|
||||
`ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'cd /opt/calvana && docker build -t calvana:latest . 2>&1 | tail -2 && docker service update --force calvana 2>&1 | tail -2'"`
|
||||
], { signal, timeout: 60000 });
|
||||
|
||||
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
state.lastDeployed = now;
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `✓ Deployed to https://${DEPLOY_CONFIG.domain}/live\n${rebuildResult.stdout}` }],
|
||||
details: { state: { ...state, lastDeployed: now } },
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Deploy error: ${err.message}` }],
|
||||
details: { state: { ...state }, error: err.message },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCall(_args, theme) {
|
||||
return new Text(theme.fg("toolTitle", theme.bold("🌐 deploy calvana")), 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, _options, theme) {
|
||||
const details = result.details as { error?: string } | undefined;
|
||||
if (details?.error) return new Text(theme.fg("error", `✗ ${details.error}`), 0, 0);
|
||||
return new Text(theme.fg("success", `✓ Live at https://${DEPLOY_CONFIG.domain}/live`), 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// COMMAND: /ships
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
pi.registerCommand("ships", {
|
||||
description: "View current Calvana shipping log",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("Requires interactive mode", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
||||
return new ShipLogComponent(state, theme, () => done());
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// COMMAND: /ship-deploy
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
pi.registerCommand("ship-deploy", {
|
||||
description: "Force deploy the Calvana site with current ship log",
|
||||
handler: async (_args, ctx) => {
|
||||
const ok = await ctx.ui.confirm("Deploy?", `Push ship log to https://${DEPLOY_CONFIG.domain}/live?`);
|
||||
if (!ok) return;
|
||||
|
||||
// Queue a deploy via the LLM
|
||||
pi.sendUserMessage("Use calvana_deploy to push the current ship log to the live site.", { deliverAs: "followUp" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// UI COMPONENT: /ships viewer
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
class ShipLogComponent {
|
||||
private state: ShipLogState;
|
||||
private theme: Theme;
|
||||
private onClose: () => void;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
constructor(state: ShipLogState, theme: Theme, onClose: () => void) {
|
||||
this.state = state;
|
||||
this.theme = theme;
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
||||
this.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
||||
|
||||
const lines: string[] = [];
|
||||
const th = this.theme;
|
||||
|
||||
lines.push("");
|
||||
lines.push(truncateToWidth(
|
||||
th.fg("borderMuted", "─".repeat(3)) +
|
||||
th.fg("accent", " 🚀 Calvana Ship Log ") +
|
||||
th.fg("borderMuted", "─".repeat(Math.max(0, width - 26))),
|
||||
width
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
// Ships
|
||||
if (this.state.ships.length === 0) {
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", "No ships yet.")}`, width));
|
||||
} else {
|
||||
const shipped = this.state.ships.filter(s => s.status === "shipped").length;
|
||||
lines.push(truncateToWidth(
|
||||
` ${th.fg("muted", `${shipped}/${this.state.ships.length} shipped`)}`,
|
||||
width
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
for (const s of this.state.ships) {
|
||||
const badge = s.status === "shipped" ? th.fg("success", "✓ SHIPPED ")
|
||||
: s.status === "shipping" ? th.fg("warning", "● SHIPPING")
|
||||
: th.fg("dim", "○ PLANNED ");
|
||||
lines.push(truncateToWidth(
|
||||
` ${badge} ${th.fg("accent", `#${s.id}`)} ${th.fg("text", s.title)}`,
|
||||
width
|
||||
));
|
||||
lines.push(truncateToWidth(
|
||||
` ${th.fg("dim", s.timestamp)} · ${th.fg("dim", s.metric)}`,
|
||||
width
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Oops
|
||||
if (this.state.oops.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(truncateToWidth(` ${th.fg("warning", "💥 Oops Log")}`, width));
|
||||
for (const o of this.state.oops) {
|
||||
lines.push(truncateToWidth(
|
||||
` ${th.fg("error", "─")} ${th.fg("muted", o.description)} ${th.fg("dim", `(${o.fixTime})`)}`,
|
||||
width
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
if (this.state.lastDeployed) {
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", `Last deployed: ${this.state.lastDeployed}`)}`, width));
|
||||
}
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
|
||||
lines.push("");
|
||||
|
||||
this.cachedWidth = width;
|
||||
this.cachedLines = lines;
|
||||
return lines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// HTML GENERATOR — Builds the /live page from current state
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
function generateLivePageHtml(state: ShipLogState): string {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const shipCards = state.ships.map(s => {
|
||||
const badgeClass = s.status === "shipped" ? "badge-shipped"
|
||||
: s.status === "shipping" ? "badge-shipping"
|
||||
: "badge-planned";
|
||||
const badgeLabel = s.status.charAt(0).toUpperCase() + s.status.slice(1);
|
||||
const titleSuffix = s.status === "shipped" ? " ✓" : "";
|
||||
|
||||
return ` <div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">${escapeHtml(s.title)}${titleSuffix}</span>
|
||||
<span class="badge ${badgeClass}">${badgeLabel}</span>
|
||||
</div>
|
||||
<p class="card-meta">⏱ ${escapeHtml(s.timestamp)}</p>
|
||||
<p class="metric">What moved: ${escapeHtml(s.metric)}</p>
|
||||
<div class="card-links"><a href="${escapeHtml(s.prLink)}">PR</a><a href="${escapeHtml(s.deployLink)}">Deploy</a><a href="${escapeHtml(s.loomLink)}">Loom clip</a></div>
|
||||
</div>`;
|
||||
}).join("\n");
|
||||
|
||||
const oopsEntries = state.oops.map(o => {
|
||||
return ` <div class="oops-entry">
|
||||
<span>${escapeHtml(o.description)}${o.fixTime !== "—" ? ` Fixed in ${escapeHtml(o.fixTime)}.` : ""}</span>
|
||||
<a href="${escapeHtml(o.commitLink)}">→ commit</a>
|
||||
</div>`;
|
||||
}).join("\n");
|
||||
|
||||
// If no ships yet, show placeholder
|
||||
const shipsSection = state.ships.length > 0 ? shipCards : ` <div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Warming up...</span>
|
||||
<span class="badge badge-planned">Planned</span>
|
||||
</div>
|
||||
<p class="card-meta">⏱ —</p>
|
||||
<p class="metric">What moved: —</p>
|
||||
</div>`;
|
||||
|
||||
const oopsSection = state.oops.length > 0 ? oopsEntries : ` <div class="oops-entry">
|
||||
<span>Nothing broken yet. Give it time.</span>
|
||||
<a href="#commit">→ waiting</a>
|
||||
</div>`;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Calvana — Live Shipping Log</title>
|
||||
<meta name="description" content="Intentional chaos. Full receipts. Watch the build happen in real time.">
|
||||
<meta property="og:title" content="Calvana — Live Shipping Log">
|
||||
<meta property="og:description" content="Intentional chaos. Full receipts.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://${DEPLOY_CONFIG.domain}/live">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<link rel="canonical" href="https://${DEPLOY_CONFIG.domain}/live">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="logo">calvana<span>.exe</span></a>
|
||||
<div class="nav-links">
|
||||
<a href="/manifesto">/manifesto</a>
|
||||
<a href="/live" class="active">/live</a>
|
||||
<a href="/hire">/hire</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="page">
|
||||
<h1 class="hero-title">Live Shipping Log</h1>
|
||||
<p class="subtitle">Intentional chaos. Full receipts.</p>
|
||||
|
||||
<section class="section">
|
||||
<h2>Today's Ships</h2>
|
||||
<div class="card-grid">
|
||||
${shipsSection}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="two-col">
|
||||
<div class="col col-broke">
|
||||
<h3>Rules I broke today</h3>
|
||||
<ul>
|
||||
<li>Didn't ask permission</li>
|
||||
<li>Didn't wait for alignment</li>
|
||||
<li>Didn't write a PRD</li>
|
||||
<li>Didn't submit a normal application</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col col-kept">
|
||||
<h3>Rules I refuse to break</h3>
|
||||
<ul>
|
||||
<li>No silent failures</li>
|
||||
<li>No unbounded AI spend</li>
|
||||
<li>No hallucinations shipped to users</li>
|
||||
<li>No deploy without rollback path</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Oops Log</h2>
|
||||
<p class="subtitle" style="margin-bottom:1rem">If it's not here, I haven't broken it yet.</p>
|
||||
<div class="oops-log">
|
||||
${oopsSection}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p class="footer-tagline">${SITE_CONFIG.tagline}</p>
|
||||
<p style="margin-top:.4rem">Last updated: ${now}</p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
60
.pi/infra.md
@@ -1,60 +0,0 @@
|
||||
# Infrastructure Access
|
||||
# All values live in `.env` (gitignored). This file maps the topology.
|
||||
|
||||
## Server
|
||||
| Var | Purpose |
|
||||
|-----|---------|
|
||||
| `SSH_USER`, `SSH_HOST`, `SSH_PORT` | Primary server SSH access |
|
||||
|
||||
## Incus Containers (on primary server)
|
||||
| Container | Internal IP | Status | Purpose |
|
||||
|-----------------|-----------------|---------|---------------|
|
||||
| cr-server-new | 10.213.16.224 | RUNNING | CharityRight |
|
||||
| qc-server-new | 10.213.16.234 | RUNNING | QuikCue |
|
||||
| qc-server | — | STOPPED | legacy |
|
||||
|
||||
## HAProxy (on primary server)
|
||||
| Domain pattern | Backend |
|
||||
|----------------------|----------------------|
|
||||
| charityright domains | → cr-server-new:443/80 |
|
||||
| quikcue domains | → qc-server-new:443/80 |
|
||||
| antivirus.quikcue.com| → localhost:8877 |
|
||||
| SSH (gitea) | → qc-server-new:2224 |
|
||||
|
||||
## Databases
|
||||
| Var | Type | Purpose |
|
||||
|-----|------|---------|
|
||||
| `DATABASE_URL` | Postgres | donation_warehouse (port 5000 on primary) |
|
||||
| `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD` | MySQL | CharityRight legacy (DigitalOcean managed) |
|
||||
| `REDIS_HOST`, `REDIS_PASSWORD`, `REDIS_PORT` | Redis | CharityRight sessions/cache |
|
||||
|
||||
## Services on Server
|
||||
| Path | Service | Key Vars |
|
||||
|------|---------|----------|
|
||||
| `/opt/ayn-antivirus` | AYN Antivirus scanner + dashboard | `ANTHROPIC_API_KEY` |
|
||||
| `/opt/enthuse-db-sync-v2` | Enthuse donation sync | `ENTHUSE_EMAIL`, `TOTP_SECRET`, `GOOGLE_CLIENT_*` |
|
||||
| `/opt/launchgood-sync` | LaunchGood donation sync | `LG_EMAIL`, `LG_PASSWORD` |
|
||||
| `/root/legacy-donation-system-laravel` | CharityRight Laravel app | `STRIPE_*`, `PAYPAL_*`, `GOCARDLESS_*`, `POSTMARK_TOKEN` |
|
||||
| `/root/redis-v2` | Redis instance | `REDIS_PASSWORD` |
|
||||
|
||||
## Payment Providers
|
||||
| Var prefix | Provider |
|
||||
|------------|----------|
|
||||
| `STRIPE_*` | Stripe (live) |
|
||||
| `PAYPAL_*` | PayPal (live) |
|
||||
| `GOCARDLESS_*` | GoCardless (live) |
|
||||
|
||||
## Mail
|
||||
| Var | Provider |
|
||||
|-----|----------|
|
||||
| `SENDGRID_TX_API_KEY` | SendGrid |
|
||||
| `POSTMARK_TOKEN` | Postmark (active mailer) |
|
||||
|
||||
## Third-party Integrations
|
||||
| Var | Service |
|
||||
|-----|---------|
|
||||
| `N3O_*_ENDPOINT` | N3O/Engage donation import hooks |
|
||||
| `ZAPIER_WEBHOOK_ENDPOINT` | Zapier automation |
|
||||
| `GOOGLE_PLACES_API_KEY` | Google Places autocomplete |
|
||||
| `CT_STRAVA_*` | Strava challenge tracker |
|
||||
| `WORDPRESS_URL`, `WORDPRESS_KEY` | WordPress (Cloudways) |
|
||||
3
.pi/observatory/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
events.jsonl
|
||||
summary.json
|
||||
report.md
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"theme": "synthwave",
|
||||
"prompts": [
|
||||
"../.claude/commands"
|
||||
]
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
name: bowser
|
||||
description: Headless browser automation using Playwright CLI. Use when you need headless browsing, parallel browser sessions, UI testing, screenshots, web scraping, or browser automation that can run in the background. Keywords - playwright, headless, browser, test, screenshot, scrape, parallel.
|
||||
allowed-tools: Bash
|
||||
---
|
||||
|
||||
# Playwright Bowser
|
||||
|
||||
## Purpose
|
||||
|
||||
Automate browsers using `playwright-cli` (via `@playwright/cli`) — a token-efficient CLI for Playwright. Runs headless by default, supports parallel sessions via named sessions (`-s=`), and doesn't load tool schemas into context.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the package is installed in the project:
|
||||
```bash
|
||||
bun add -d @playwright/cli
|
||||
bunx playwright install chromium
|
||||
```
|
||||
|
||||
## Key Details
|
||||
|
||||
- **Headless by default** — pass `--headed` to `open` to see the browser
|
||||
- **Parallel sessions** — use `-s=<name>` to run multiple independent browser instances
|
||||
- **Persistent profiles** — cookies and storage state preserved between calls
|
||||
- **Token-efficient** — CLI-based, no accessibility trees or tool schemas in context
|
||||
- **Vision mode** (opt-in) — set `PLAYWRIGHT_MCP_CAPS=vision` to receive screenshots as image responses in context instead of just saving to disk
|
||||
|
||||
## Sessions
|
||||
|
||||
**Always use a named session.** Derive a short, descriptive kebab-case name from the user's prompt. This gives each task a persistent browser profile (cookies, localStorage, history) that accumulates across calls.
|
||||
|
||||
```bash
|
||||
# Derive session name from prompt context:
|
||||
# "test the checkout flow on mystore.com" → -s=mystore-checkout
|
||||
# "scrape pricing from competitor.com" → -s=competitor-pricing
|
||||
# "UI test the login page" → -s=login-ui-test
|
||||
|
||||
bunx playwright-cli -s=mystore-checkout open https://mystore.com --persistent
|
||||
bunx playwright-cli -s=mystore-checkout snapshot
|
||||
bunx playwright-cli -s=mystore-checkout click e12
|
||||
```
|
||||
|
||||
Managing sessions:
|
||||
```bash
|
||||
bunx playwright-cli list # list all sessions
|
||||
bunx playwright-cli close-all # close all sessions
|
||||
bunx playwright-cli -s=<name> close # close specific session
|
||||
bunx playwright-cli -s=<name> delete-data # wipe session profile
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
Core: open [url], goto <url>, click <ref>, fill <ref> <text>, type <text>, snapshot, screenshot [ref], close
|
||||
Navigate: go-back, go-forward, reload
|
||||
Keyboard: press <key>, keydown <key>, keyup <key>
|
||||
Mouse: mousemove <x> <y>, mousedown, mouseup, mousewheel <dx> <dy>
|
||||
Tabs: tab-list, tab-new [url], tab-close [index], tab-select <index>
|
||||
Save: screenshot [ref], pdf, screenshot --filename=f
|
||||
Storage: state-save, state-load, cookie-*, localstorage-*, sessionstorage-*
|
||||
Network: route <pattern>, route-list, unroute, network
|
||||
DevTools: console, run-code <code>, tracing-start/stop, video-start/stop
|
||||
Sessions: -s=<name> <cmd>, list, close-all, kill-all
|
||||
Config: open --headed, open --browser=chrome, resize <w> <h>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Derive a session name from the user's prompt and open with `--persistent` to preserve cookies/state. Always set the viewport via env var at launch:
|
||||
```bash
|
||||
PLAYWRIGHT_MCP_VIEWPORT_SIZE=1440x900 bunx playwright-cli -s=<session-name> open <url> --persistent
|
||||
# or headed:
|
||||
PLAYWRIGHT_MCP_VIEWPORT_SIZE=1440x900 bunx playwright-cli -s=<session-name> open <url> --persistent --headed
|
||||
# or with vision (screenshots returned as image responses in context):
|
||||
PLAYWRIGHT_MCP_VIEWPORT_SIZE=1440x900 PLAYWRIGHT_MCP_CAPS=vision bunx playwright-cli -s=<session-name> open <url> --persistent
|
||||
```
|
||||
|
||||
3. Get element references via snapshot:
|
||||
```bash
|
||||
bunx playwright-cli snapshot
|
||||
```
|
||||
|
||||
4. Interact using refs from snapshot:
|
||||
```bash
|
||||
bunx playwright-cli click <ref>
|
||||
bunx playwright-cli fill <ref> "text"
|
||||
bunx playwright-cli type "text"
|
||||
bunx playwright-cli press Enter
|
||||
```
|
||||
|
||||
5. Capture results:
|
||||
```bash
|
||||
bunx playwright-cli screenshot
|
||||
bunx playwright-cli screenshot --filename=output.png
|
||||
```
|
||||
|
||||
6. **Always close the session when done.** This is not optional — close the named session after finishing your task:
|
||||
```bash
|
||||
bunx playwright-cli -s=<session-name> close
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
If a `playwright-cli.json` exists in the working directory, use it automatically. If the user provides a path to a config file, use `--config path/to/config.json`. Otherwise, skip configuration — the env var and CLI defaults are sufficient.
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": {
|
||||
"browserName": "chromium",
|
||||
"launchOptions": { "headless": true },
|
||||
"contextOptions": { "viewport": { "width": 1440, "height": 900 } }
|
||||
},
|
||||
"outputDir": "./screenshots"
|
||||
}
|
||||
```
|
||||
|
||||
## Full Help
|
||||
|
||||
Run `bunx playwright-cli --help` or `bunx playwright-cli --help <command>` for detailed command usage.
|
||||
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "catppuccin-mocha",
|
||||
"vars": {
|
||||
"bg": "#1e1e2e",
|
||||
"bgDark": "#181825",
|
||||
"bgDeep": "#13131e",
|
||||
"surface": "#2a2a3c",
|
||||
"selection": "#34344a",
|
||||
"bgRed": "#2e1420",
|
||||
"bgGreen": "#142218",
|
||||
"bgPeach": "#2e2010",
|
||||
"bgBlue": "#141e38",
|
||||
"bgMauve": "#261840",
|
||||
"bgTeal": "#122830",
|
||||
"comment": "#d5bcff",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"red": "#ff7eb3",
|
||||
"maroon": "#ffa0b8",
|
||||
"peach": "#ffb370",
|
||||
"yellow": "#ffe585",
|
||||
"green": "#7af5a0",
|
||||
"teal": "#60f0d8",
|
||||
"sky": "#6ae4ff",
|
||||
"sapphire": "#5cceff",
|
||||
"blue": "#7db8ff",
|
||||
"lavender": "#bfb8ff",
|
||||
"mauve": "#d9a0ff",
|
||||
"flamingo": "#ffc4c4",
|
||||
"pink": "#ffb0e0"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "mauve",
|
||||
"border": "selection",
|
||||
"borderAccent": "mauve",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "teal",
|
||||
"selectedBg": "bgMauve",
|
||||
"userMessageBg": "bgBlue",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgTeal",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "teal",
|
||||
"toolPendingBg": "bgPeach",
|
||||
"toolSuccessBg": "bgGreen",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "peach",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "peach",
|
||||
"mdLink": "blue",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "sky",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "green",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "mauve",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "mauve",
|
||||
"syntaxFunction": "blue",
|
||||
"syntaxVariable": "pink",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "peach",
|
||||
"syntaxType": "sky",
|
||||
"syntaxOperator": "lavender",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "sky",
|
||||
"thinkingHigh": "mauve",
|
||||
"thinkingXhigh": "red",
|
||||
"bashMode": "yellow"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "cyberpunk",
|
||||
"vars": {
|
||||
"bg": "#0a0a14",
|
||||
"bgDark": "#06060e",
|
||||
"bgDeep": "#040410",
|
||||
"surface": "#12122a",
|
||||
"selection": "#1a1a38",
|
||||
"bgRed": "#2a0a12",
|
||||
"bgOrange": "#2a1408",
|
||||
"bgSky": "#081a30",
|
||||
"bgCyan": "#0a2228",
|
||||
"bgWarm": "#220a30",
|
||||
"bgPink": "#2a0a22",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"comment": "#ffe600",
|
||||
"yellow": "#ffe600",
|
||||
"cyan": "#00e5ff",
|
||||
"magenta": "#ff00aa",
|
||||
"red": "#ff1744",
|
||||
"green": "#00e676",
|
||||
"purple": "#aa00ff",
|
||||
"blue": "#2979ff",
|
||||
"orange": "#ff6d00"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "cyan",
|
||||
"border": "magenta",
|
||||
"borderAccent": "yellow",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "orange",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "green",
|
||||
"selectedBg": "bgPink",
|
||||
"userMessageBg": "bgWarm",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "cyan",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgSky",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "yellow",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "magenta",
|
||||
"mdLink": "cyan",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "green",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "purple",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "yellow",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "magenta",
|
||||
"syntaxFunction": "cyan",
|
||||
"syntaxVariable": "yellow",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "purple",
|
||||
"syntaxType": "blue",
|
||||
"syntaxOperator": "magenta",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "purple",
|
||||
"thinkingHigh": "cyan",
|
||||
"thinkingXhigh": "magenta",
|
||||
"bashMode": "orange"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "dracula",
|
||||
"vars": {
|
||||
"bg": "#1a1b26",
|
||||
"bgDark": "#161722",
|
||||
"bgDeep": "#141520",
|
||||
"surface": "#252738",
|
||||
"selection": "#2c2e44",
|
||||
"bgRed": "#2e1220",
|
||||
"bgOrange": "#2e1c12",
|
||||
"bgGreen": "#122e1a",
|
||||
"bgCyan": "#122a2e",
|
||||
"bgPurple": "#261536",
|
||||
"bgPink": "#2e1228",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"comment": "#f8fcc4",
|
||||
"cyan": "#8be9fd",
|
||||
"green": "#50fa7b",
|
||||
"orange": "#ffb86c",
|
||||
"pink": "#ff79c6",
|
||||
"purple": "#bd93f9",
|
||||
"red": "#ff5555",
|
||||
"yellow": "#f1fa8c",
|
||||
"blue": "#6296e4"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "purple",
|
||||
"border": "pink",
|
||||
"borderAccent": "purple",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "orange",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "cyan",
|
||||
"selectedBg": "bgPurple",
|
||||
"userMessageBg": "bgPink",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "cyan",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgGreen",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "pink",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "pink",
|
||||
"mdLink": "cyan",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "green",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "purple",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "pink",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "pink",
|
||||
"syntaxFunction": "green",
|
||||
"syntaxVariable": "fg",
|
||||
"syntaxString": "yellow",
|
||||
"syntaxNumber": "purple",
|
||||
"syntaxType": "cyan",
|
||||
"syntaxOperator": "pink",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "purple",
|
||||
"thinkingHigh": "cyan",
|
||||
"thinkingXhigh": "pink",
|
||||
"bashMode": "orange"
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "everforest",
|
||||
"vars": {
|
||||
"bg": "#191f1d",
|
||||
"bgDark": "#141a18",
|
||||
"bg1": "#1e2522",
|
||||
"bg2": "#222a28",
|
||||
"surface": "#2c3532",
|
||||
"selection": "#323e3a",
|
||||
"bgRed": "#301718",
|
||||
"bgOrange": "#302217",
|
||||
"bgSky": "#192b34",
|
||||
"bgCyan": "#172b26",
|
||||
"bgWarm": "#351d29",
|
||||
"bgPink": "#311c31",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"comment": "#e7f4cd",
|
||||
"red": "#eb7073",
|
||||
"orange": "#f1a27e",
|
||||
"yellow": "#eed096",
|
||||
"green": "#bde481",
|
||||
"aqua": "#78e292",
|
||||
"teal": "#52e0bd",
|
||||
"blue": "#78c8e2",
|
||||
"purple": "#e689b5"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "green",
|
||||
"border": "aqua",
|
||||
"borderAccent": "green",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "orange",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "teal",
|
||||
"selectedBg": "bgCyan",
|
||||
"userMessageBg": "bgWarm",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgSky",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "aqua",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgCyan",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "green",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "blue",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "aqua",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "teal",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "green",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "red",
|
||||
"syntaxFunction": "green",
|
||||
"syntaxVariable": "blue",
|
||||
"syntaxString": "yellow",
|
||||
"syntaxNumber": "purple",
|
||||
"syntaxType": "aqua",
|
||||
"syntaxOperator": "orange",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "teal",
|
||||
"thinkingHigh": "green",
|
||||
"thinkingXhigh": "red",
|
||||
"bashMode": "orange"
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "gruvbox",
|
||||
"vars": {
|
||||
"bg": "#221f1c",
|
||||
"bgDark": "#1c1a17",
|
||||
"bgDeep": "#171412",
|
||||
"surface": "#322d29",
|
||||
"selection": "#3f3731",
|
||||
"bgRed": "#341714",
|
||||
"bgOrange": "#322215",
|
||||
"bgSky": "#152432",
|
||||
"bgCyan": "#142924",
|
||||
"bgWarm": "#322b15",
|
||||
"bgPink": "#321524",
|
||||
"comment": "#fcebc5",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"red": "#fb4b37",
|
||||
"green": "#ebed5e",
|
||||
"yellow": "#fcd783",
|
||||
"blue": "#67a6e4",
|
||||
"purple": "#ca74e7",
|
||||
"aqua": "#81e4be",
|
||||
"orange": "#fd953f"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "orange",
|
||||
"border": "yellow",
|
||||
"borderAccent": "orange",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "aqua",
|
||||
"selectedBg": "bgWarm",
|
||||
"userMessageBg": "bgOrange",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "aqua",
|
||||
"toolPendingBg": "bgSky",
|
||||
"toolSuccessBg": "bgCyan",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "orange",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "aqua",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "green",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "blue",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "orange",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "red",
|
||||
"syntaxFunction": "aqua",
|
||||
"syntaxVariable": "blue",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "purple",
|
||||
"syntaxType": "yellow",
|
||||
"syntaxOperator": "orange",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "aqua",
|
||||
"thinkingHigh": "yellow",
|
||||
"thinkingXhigh": "red",
|
||||
"bashMode": "orange"
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "midnight-ocean",
|
||||
"vars": {
|
||||
"deepBlue": "#0a192f",
|
||||
"oceanBlue": "#0077be",
|
||||
"teal": "#00ced1",
|
||||
"cyan": "#4fd1ed",
|
||||
"softWhite": "#e6f1ff",
|
||||
"mutedBlue": "#233554",
|
||||
"lightMutedBlue": "#a8b2d1",
|
||||
"slate": "#8892b0",
|
||||
"successGreen": "#64ffda",
|
||||
"errorRed": "#ff5f56",
|
||||
"warningAmber": "#ffd700",
|
||||
"purple": "#c678dd"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "oceanBlue",
|
||||
"border": "mutedBlue",
|
||||
"borderAccent": "teal",
|
||||
"borderMuted": 236,
|
||||
"success": "successGreen",
|
||||
"error": "errorRed",
|
||||
"warning": "warningAmber",
|
||||
"muted": "slate",
|
||||
"dim": 240,
|
||||
"text": "softWhite",
|
||||
"thinkingText": "teal",
|
||||
"selectedBg": "#112240",
|
||||
"userMessageBg": "#112240",
|
||||
"userMessageText": "softWhite",
|
||||
"customMessageBg": "#112240",
|
||||
"customMessageText": "softWhite",
|
||||
"customMessageLabel": "teal",
|
||||
"toolPendingBg": "deepBlue",
|
||||
"toolSuccessBg": "#0d2521",
|
||||
"toolErrorBg": "#331616",
|
||||
"toolTitle": "cyan",
|
||||
"toolOutput": "lightMutedBlue",
|
||||
"mdHeading": "teal",
|
||||
"mdLink": "oceanBlue",
|
||||
"mdLinkUrl": "slate",
|
||||
"mdCode": "cyan",
|
||||
"mdCodeBlock": "#011627",
|
||||
"mdCodeBlockBorder": "mutedBlue",
|
||||
"mdQuote": "slate",
|
||||
"mdQuoteBorder": "mutedBlue",
|
||||
"mdHr": "mutedBlue",
|
||||
"mdListBullet": "teal",
|
||||
"toolDiffAdded": "successGreen",
|
||||
"toolDiffRemoved": "errorRed",
|
||||
"toolDiffContext": "slate",
|
||||
"syntaxComment": "slate",
|
||||
"syntaxKeyword": "purple",
|
||||
"syntaxFunction": "teal",
|
||||
"syntaxVariable": "cyan",
|
||||
"syntaxString": "successGreen",
|
||||
"syntaxNumber": "warningAmber",
|
||||
"syntaxType": "oceanBlue",
|
||||
"syntaxOperator": "teal",
|
||||
"syntaxPunctuation": "lightMutedBlue",
|
||||
"thinkingOff": "mutedBlue",
|
||||
"thinkingMinimal": "oceanBlue",
|
||||
"thinkingLow": "teal",
|
||||
"thinkingMedium": "cyan",
|
||||
"thinkingHigh": "warningAmber",
|
||||
"thinkingXhigh": "errorRed",
|
||||
"bashMode": "warningAmber"
|
||||
},
|
||||
"export": {
|
||||
"pageBg": "#0a192f",
|
||||
"cardBg": "#112240",
|
||||
"infoBg": "#0077be"
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "nord",
|
||||
"vars": {
|
||||
"bg": "#1a1d23",
|
||||
"bgDark": "#15181d",
|
||||
"bgDeep": "#111316",
|
||||
"surface": "#272b34",
|
||||
"selection": "#2f3541",
|
||||
"bgRed": "#2e1818",
|
||||
"bgOrange": "#31241a",
|
||||
"bgSky": "#1c2835",
|
||||
"bgCyan": "#192c2d",
|
||||
"bgWarm": "#291b30",
|
||||
"bgPink": "#2d1927",
|
||||
"comment": "#ccebf4",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"frost1": "#67e4e2",
|
||||
"frost2": "#72cee8",
|
||||
"frost3": "#67a5e4",
|
||||
"frost4": "#5c97df",
|
||||
"red": "#e85e6c",
|
||||
"orange": "#ed7f5e",
|
||||
"yellow": "#f5d189",
|
||||
"green": "#92df6b",
|
||||
"purple": "#e278e2",
|
||||
"border": "#3e5974",
|
||||
"dim": "#3d4c5b"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "frost2",
|
||||
"border": "border",
|
||||
"borderAccent": "frost2",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "orange",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "frost1",
|
||||
"selectedBg": "bgPink",
|
||||
"userMessageBg": "bgWarm",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "frost2",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgSky",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "orange",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "frost2",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "frost1",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "purple",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "frost2",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "frost3",
|
||||
"syntaxFunction": "frost2",
|
||||
"syntaxVariable": "fg",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "purple",
|
||||
"syntaxType": "frost1",
|
||||
"syntaxOperator": "frost3",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "dim",
|
||||
"thinkingLow": "frost4",
|
||||
"thinkingMedium": "frost3",
|
||||
"thinkingHigh": "frost2",
|
||||
"thinkingXhigh": "frost1",
|
||||
"bashMode": "yellow"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "ocean-breeze",
|
||||
"vars": {
|
||||
"bg": "#0d1b2a",
|
||||
"bgDark": "#0a1520",
|
||||
"bgDeep": "#081018",
|
||||
"surface": "#152a3e",
|
||||
"selection": "#1b3450",
|
||||
"bgRed": "#2a1018",
|
||||
"bgOrange": "#2a1e10",
|
||||
"bgSky": "#0e2440",
|
||||
"bgCyan": "#0c2a2e",
|
||||
"bgWarm": "#2a1530",
|
||||
"bgPink": "#2e1028",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"comment": "#c2faf2",
|
||||
"coral": "#ff6b6b",
|
||||
"amber": "#ffd166",
|
||||
"kelp": "#2eeab5",
|
||||
"biolum": "#33fff7",
|
||||
"foam": "#50b0e0",
|
||||
"spray": "#7ec8e3",
|
||||
"mist": "#a8d8ea",
|
||||
"sand": "#ecf49a",
|
||||
"purple": "#b48aef",
|
||||
"pink": "#f772b9"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "biolum",
|
||||
"border": "foam",
|
||||
"borderAccent": "biolum",
|
||||
"borderMuted": "surface",
|
||||
"success": "kelp",
|
||||
"error": "coral",
|
||||
"warning": "amber",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "biolum",
|
||||
"selectedBg": "selection",
|
||||
"userMessageBg": "bgSky",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "spray",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgCyan",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "spray",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "mist",
|
||||
"mdLink": "biolum",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "kelp",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "purple",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "spray",
|
||||
"toolDiffAdded": "kelp",
|
||||
"toolDiffRemoved": "coral",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "coral",
|
||||
"syntaxFunction": "biolum",
|
||||
"syntaxVariable": "spray",
|
||||
"syntaxString": "kelp",
|
||||
"syntaxNumber": "amber",
|
||||
"syntaxType": "purple",
|
||||
"syntaxOperator": "foam",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "foam",
|
||||
"thinkingMedium": "spray",
|
||||
"thinkingHigh": "biolum",
|
||||
"thinkingXhigh": "pink",
|
||||
"bashMode": "amber"
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "rose-pine",
|
||||
"vars": {
|
||||
"bg": "#1a1726",
|
||||
"bgDark": "#161320",
|
||||
"bgDeep": "#12101c",
|
||||
"surface": "#242038",
|
||||
"selection": "#2e2946",
|
||||
"bgRed": "#2c1220",
|
||||
"bgOrange": "#2a1c12",
|
||||
"bgSky": "#122030",
|
||||
"bgCyan": "#132a2e",
|
||||
"bgWarm": "#2a1830",
|
||||
"bgPink": "#301828",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"comment": "#f0a8be",
|
||||
"love": "#f47a9e",
|
||||
"gold": "#f8cc85",
|
||||
"rose": "#f0c4c4",
|
||||
"pine": "#50b8d8",
|
||||
"foam": "#a8e0ea",
|
||||
"iris": "#d4a8ff",
|
||||
"orchid": "#e088d0",
|
||||
"ember": "#f09060",
|
||||
"green": "#78e0a0"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "iris",
|
||||
"border": "orchid",
|
||||
"borderAccent": "iris",
|
||||
"borderMuted": "surface",
|
||||
"success": "foam",
|
||||
"error": "love",
|
||||
"warning": "gold",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "foam",
|
||||
"selectedBg": "bgPink",
|
||||
"userMessageBg": "bgWarm",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "iris",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgSky",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "gold",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "love",
|
||||
"mdLink": "foam",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "gold",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "rose",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "iris",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "love",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "love",
|
||||
"syntaxFunction": "foam",
|
||||
"syntaxVariable": "fg",
|
||||
"syntaxString": "gold",
|
||||
"syntaxNumber": "iris",
|
||||
"syntaxType": "pine",
|
||||
"syntaxOperator": "orchid",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "pine",
|
||||
"thinkingMedium": "iris",
|
||||
"thinkingHigh": "foam",
|
||||
"thinkingXhigh": "love",
|
||||
"bashMode": "ember"
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "synthwave",
|
||||
"vars": {
|
||||
"bg": "#262335",
|
||||
"bgDark": "#241b2f",
|
||||
"bgDeep": "#1e1d2d",
|
||||
"surface": "#34294f",
|
||||
"selection": "#463465",
|
||||
"bgRed": "#3d1018",
|
||||
"bgRedWarm": "#301510",
|
||||
"bgOrange": "#2e1f10",
|
||||
"bgSky": "#1a2e4a",
|
||||
"bgCyan": "#152838",
|
||||
"bgWarm": "#4a1e6a",
|
||||
"bgPink": "#35153a",
|
||||
"comment": "#fede5d",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"red": "#fe4450",
|
||||
"cyan": "#36f9f6",
|
||||
"yellow": "#fede5d",
|
||||
"pink": "#ff7edb",
|
||||
"green": "#72f1b8",
|
||||
"orange": "#ff8b39",
|
||||
"purple": "#c792ea",
|
||||
"blue": "#4d9de0"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "cyan",
|
||||
"border": "pink",
|
||||
"borderAccent": "cyan",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "orange",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "#4a9e6a",
|
||||
"selectedBg": "bgPink",
|
||||
"userMessageBg": "bgWarm",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "cyan",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgSky",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "orange",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "cyan",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "yellow",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "purple",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "pink",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "red",
|
||||
"syntaxFunction": "cyan",
|
||||
"syntaxVariable": "fg",
|
||||
"syntaxString": "yellow",
|
||||
"syntaxNumber": "pink",
|
||||
"syntaxType": "green",
|
||||
"syntaxOperator": "cyan",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "purple",
|
||||
"thinkingHigh": "cyan",
|
||||
"thinkingXhigh": "pink",
|
||||
"bashMode": "orange"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "tokyo-night",
|
||||
"vars": {
|
||||
"bg": "#1a1b26",
|
||||
"bgDark": "#141520",
|
||||
"bg1": "#1e2030",
|
||||
"bg2": "#252840",
|
||||
"surface": "#2a2d48",
|
||||
"selection": "#353860",
|
||||
"bgRed": "#301420",
|
||||
"bgOrange": "#2e1e14",
|
||||
"bgSky": "#162040",
|
||||
"bgCyan": "#142530",
|
||||
"bgWarm": "#301848",
|
||||
"bgPink": "#2d1430",
|
||||
"comment": "#90e8ff",
|
||||
"fg": "#ffffff",
|
||||
"fgSoft": "#bbbbbb",
|
||||
"blue": "#7eaaff",
|
||||
"cyan": "#72dfff",
|
||||
"magenta": "#c9a5ff",
|
||||
"purple": "#b48ef5",
|
||||
"green": "#a8e06a",
|
||||
"red": "#ff7a94",
|
||||
"orange": "#ffa55c",
|
||||
"yellow": "#f0c060",
|
||||
"teal": "#20d4b0"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "blue",
|
||||
"border": "purple",
|
||||
"borderAccent": "cyan",
|
||||
"borderMuted": "surface",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "orange",
|
||||
"muted": "comment",
|
||||
"dim": "comment",
|
||||
"text": "fg",
|
||||
"thinkingText": "teal",
|
||||
"selectedBg": "bgPink",
|
||||
"userMessageBg": "bgWarm",
|
||||
"userMessageText": "fg",
|
||||
"customMessageBg": "bgCyan",
|
||||
"customMessageText": "fg",
|
||||
"customMessageLabel": "cyan",
|
||||
"toolPendingBg": "bgOrange",
|
||||
"toolSuccessBg": "bgSky",
|
||||
"toolErrorBg": "bgRed",
|
||||
"toolTitle": "orange",
|
||||
"toolOutput": "fgSoft",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "cyan",
|
||||
"mdLinkUrl": "comment",
|
||||
"mdCode": "magenta",
|
||||
"mdCodeBlock": "fgSoft",
|
||||
"mdCodeBlockBorder": "surface",
|
||||
"mdQuote": "green",
|
||||
"mdQuoteBorder": "surface",
|
||||
"mdHr": "surface",
|
||||
"mdListBullet": "blue",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "comment",
|
||||
"syntaxComment": "comment",
|
||||
"syntaxKeyword": "magenta",
|
||||
"syntaxFunction": "blue",
|
||||
"syntaxVariable": "purple",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "orange",
|
||||
"syntaxType": "cyan",
|
||||
"syntaxOperator": "teal",
|
||||
"syntaxPunctuation": "fgSoft",
|
||||
"thinkingOff": "surface",
|
||||
"thinkingMinimal": "comment",
|
||||
"thinkingLow": "blue",
|
||||
"thinkingMedium": "cyan",
|
||||
"thinkingHigh": "magenta",
|
||||
"thinkingXhigh": "red",
|
||||
"bashMode": "yellow"
|
||||
}
|
||||
}
|
||||
210
AUDIT.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# JustVitamin Proposal Site — Conversion Audit
|
||||
|
||||
**Audited:** justvitamin.quikcue.com (/, /proposal, /offer, /dashboard)
|
||||
**Data source:** PostgreSQL DB — 728,018 validated orders, Nov 2005 – Jan 2026
|
||||
**Auditor:** Ruthless QA pass — every claim verified against raw data
|
||||
|
||||
---
|
||||
|
||||
## Start-Today Score: 3/10
|
||||
|
||||
**The demos are genuinely impressive but the two CTAs that matter — "Approve & Start Build" and "Let's Go" — are both broken `mailto:` links with NO email address. A client literally cannot say yes.**
|
||||
|
||||
---
|
||||
|
||||
## Top 10 Fixes (Highest Leverage First)
|
||||
|
||||
### Fix #1: BROKEN CTAs — Both conversion buttons are dead links
|
||||
- **Problem:** `/offer` "Approve & Start Build →" and `/proposal` "Let's Go →" both link to `mailto:` with no email address. They do nothing.
|
||||
- **Why it kills conversions:** The client reaches the end of the best pitch you'll ever make — and the door is locked. 100% of decision-ready traffic dies here.
|
||||
- **Exact change:** Replace both with `mailto:omair@quikcue.com?subject=JustVitamin%20—%20Approved%20to%20Start%20Build&body=Hi%20Omair%2C%0A%0AApproved%20to%20proceed.%20Let%27s%20schedule%20the%20kickoff.` AND add a Calendly/Cal.com booking link as primary CTA.
|
||||
- **Where:** `/offer` line 552, `/proposal` line 1013
|
||||
- **Effort:** S | **Impact:** CRITICAL
|
||||
|
||||
### Fix #2: "97.4% Channel Dependency" claim is WRONG
|
||||
- **Problem:** The offer page headline claims "Organic + Google Ads = 97.4% of all orders." Actual data shows **85.4% in 2025** (Organic 56.6% + Google Ads 28.8%) or **81.7% all-time**.
|
||||
- **Why it kills conversions:** If the client checks this against their own Shopify analytics, your entire credibility collapses. One wrong number invalidates all numbers.
|
||||
- **Exact change:** Replace "97.4%" with "85%" and reframe: "85% of your orders come from just two Google-dependent channels. Facebook is 0.1%. TikTok is 0%. You have almost zero social discovery." — the story is still devastating at 85%.
|
||||
- **Where:** `/offer` hero stat box, section heading "97% Channel Dependency", channel donut chart, board summary bullet. 4 occurrences.
|
||||
- **Effort:** S | **Impact:** HIGH
|
||||
|
||||
### Fix #3: "37.3% repeat rate" is UNVERIFIED — wrong metric used
|
||||
- **Problem:** The revenue model uses "Repeat rate = 37.3% (your actual)" but this number cannot be derived from the data. The actual returning-order rate is 68.1% all-time / 86.8% in 2025. The cohort 12-month return rate averages 57.6%.
|
||||
- **Why it kills conversions:** If 37.3% is wrong, the entire ROI calculator is wrong. The interactive model is the strongest close on the page — it must be bulletproof.
|
||||
- **Exact change:** Use a verifiable metric: either cohort-based "57.6% of customers return within 12 months" or returning-order-share "68% of all orders are repeat purchases." Then recalculate the ROI model with the correct figure.
|
||||
- **Where:** `/offer` interactive revenue model, assumptions text
|
||||
- **Effort:** M | **Impact:** HIGH
|
||||
|
||||
### Fix #4: No person, no face, no credibility
|
||||
- **Problem:** Zero information about who built this. "QuikCue" and "Omair" appear in tiny footer text. No bio, no photo, no LinkedIn, no portfolio, no "why me."
|
||||
- **Why it kills conversions:** The client is being asked to pay £4,000 to someone with no visible identity. At this price point, they Google you. If they find nothing, they don't buy.
|
||||
- **Exact change:** Add a "Built by" section with: headshot, name, 2-line bio ("I've built AI systems for X, Y, Z"), LinkedIn link, and 1-2 sentence personal note to the client. Place it before the CTA on `/offer`.
|
||||
- **Where:** `/offer` before the "Decide" section, `/` above footer
|
||||
- **Effort:** S | **Impact:** HIGH
|
||||
|
||||
### Fix #5: Two competing proposal pages — pick one
|
||||
- **Problem:** `/proposal` and `/offer` are separate pages covering the same content. `/proposal` is weaker (no data story, no calculator, no de-risk section). A confused client reads both and trusts neither.
|
||||
- **Why it kills conversions:** Split attention = no action. The client doesn't know which is the "real" proposal.
|
||||
- **Exact change:** Kill `/proposal`. Redirect to `/offer`. The offer page is the complete pitch. Remove "Proposal" from nav, rename nav link to "The Proposal" pointing at `/offer`.
|
||||
- **Where:** Navigation bar, `/proposal` route
|
||||
- **Effort:** S | **Impact:** HIGH
|
||||
|
||||
### Fix #6: No pre-generated demo output — visitor must wait 90+ seconds
|
||||
- **Problem:** All 3 demos start with "Waiting." The visitor must click, then wait 70-90s for AI generation. Most visitors won't wait.
|
||||
- **Why it kills conversions:** The demo is the proof. If the proof requires patience, it's not proof — it's a promise.
|
||||
- **Exact change:** Pre-generate one demo output (the D3+K2 product) and display it as the default state. Add a "Try another product" toggle that runs the live demo. The pre-loaded output proves it works; the live toggle proves it's real.
|
||||
- **Where:** `/` Demo A section
|
||||
- **Effort:** M | **Impact:** HIGH
|
||||
|
||||
### Fix #7: Homepage hero talks to us, not to the client
|
||||
- **Problem:** "Your content engine is real and running" is about us proving our tech works. It says nothing about the client's problem, pain, or gain.
|
||||
- **Why it kills conversions:** The client's first 5 seconds should be "they understand my problem." Instead, they get "look what I built."
|
||||
- **Exact change:** Hero headline: **"JustVitamins has lost 84% of its new customers since 2020. This AI engine gets them back."** Sub: "We analysed your 728,018 orders. The product isn't the problem — discovery is. See the data, see the engine, decide today."
|
||||
- **Where:** `/` hero section (h1 + subtitle)
|
||||
- **Effort:** S | **Impact:** HIGH
|
||||
|
||||
### Fix #8: No "cost of doing nothing" visualisation
|
||||
- **Problem:** The "£5,000–£10,000 per month" cost-of-waiting claim is an ASSUMPTION with no derivation shown. It's presented as data but is actually a guess.
|
||||
- **Why it kills conversions:** Savvy buyers spot unsubstantiated urgency and distrust the rest.
|
||||
- **Exact change:** Replace with a verifiable projection: "In 2020, you acquired 24,666 new customers. In 2025, just 3,941. At your current AOV of £35.02, that's **£726,000 in lost first-purchase revenue per year** — before repeat purchases." Show the math inline. This is SOURCE-LINKED and devastating.
|
||||
- **Where:** `/offer` cost-of-waiting callout
|
||||
- **Effort:** S | **Impact:** MED
|
||||
|
||||
### Fix #9: No before/after proof of AI quality
|
||||
- **Problem:** The demos generate output live, but there's no screenshot or example showing "Here's what your PDP looks like now → Here's what it looks like after AI." The client can't visualise the transformation without running the demo.
|
||||
- **Why it kills conversions:** Before/after is the #1 conversion mechanic in any transformation pitch. It's completely absent.
|
||||
- **Exact change:** Add a 2-column "Before → After" screenshot block below Demo A. Left: actual justvitamins.co.uk PDP (screenshotted). Right: AI-generated PDP output (screenshotted from the demo). Static images, instant load.
|
||||
- **Where:** `/` between Demo A and Demo B
|
||||
- **Effort:** M | **Impact:** MED
|
||||
|
||||
### Fix #10: Revenue model "payback period" math is wrong
|
||||
- **Problem:** The calculator shows "2.6 mo" payback at 100 new customers/month, but the actual math gives 3.5 months (£12,400 / £3,502 per month). The 2.6 figure seems to include repeat revenue in month 1, which hasn't happened yet.
|
||||
- **Why it kills conversions:** If the client runs the numbers themselves and gets a different answer, trust dies.
|
||||
- **Exact change:** Use first-purchase-only for payback: 3.5 months at 100/mo, 7 months at 50/mo. Show the formula visibly. Add a note: "Repeat purchases improve ROI further in months 4-12 but are excluded from payback calculation."
|
||||
- **Where:** `/offer` interactive revenue model
|
||||
- **Effort:** S | **Impact:** MED
|
||||
|
||||
---
|
||||
|
||||
## Trust & Data Audit Report
|
||||
|
||||
### Hard Claims Table
|
||||
|
||||
| # | Claim | Page | Actual Value | Status | Action |
|
||||
|---|-------|------|-------------|--------|--------|
|
||||
| 1 | £19.4M lifetime revenue | / hero | £19,417,899 | ✅ SOURCE-LINKED | Keep |
|
||||
| 2 | 728K orders processed | / hero | 728,018 | ✅ SOURCE-LINKED | Keep |
|
||||
| 3 | 230K unique customers | / hero | 230,651 | ✅ SOURCE-LINKED | Keep |
|
||||
| 4 | 20 years trading history | / hero | Nov 2005 – Jan 2026 (20.2 yrs) | ✅ SOURCE-LINKED | Keep |
|
||||
| 5 | -84% new customer decline | /offer hero | -84.0% (24,666→3,941, 2020→2025) | ✅ SOURCE-LINKED | Keep |
|
||||
| 6 | -42% revenue from peak | /offer hero | -42.5% (£1.82M→£1.05M) | ✅ SOURCE-LINKED | Keep |
|
||||
| 7 | 97.4% channel dependency (Google+Organic) | /offer hero + 3 more | 85.4% (2025) / 81.7% (all-time) | ❌ WRONG | **Fix to 85%** |
|
||||
| 8 | AOV climbed from £26→£35 | /offer data | £26.46 (2018)→£35.02 (2025) | ✅ SOURCE-LINKED | Keep, add years |
|
||||
| 9 | Repeat rate 37% / 37.3% | /offer model | Cannot verify. Returning rate=68.1%, cohort=57.6% | ⚠️ UNVERIFIED | **Fix: use verifiable metric** |
|
||||
| 10 | 24,600/year in 2020 new customers | /offer data | 24,666 | ✅ SOURCE-LINKED | Keep |
|
||||
| 11 | Under 4,000 in 2025 new customers | /offer data | 3,941 | ✅ SOURCE-LINKED | Keep |
|
||||
| 12 | Facebook: 0.1% | /offer channel | 694/728,018 = 0.10% (all-time), 34/29,919 = 0.11% (2025) | ✅ SOURCE-LINKED | Keep |
|
||||
| 13 | TikTok: 0%, Instagram: 0% | /offer channel | Not present in channel data | ✅ DERIVED (absence=0) | Keep |
|
||||
| 14 | £5,000–£10,000/month cost of waiting | /offer callout | Requires 143-286 new social customers/month. No basis for this range. | ❌ UNVERIFIED | **Replace with verifiable calc** |
|
||||
| 15 | AOV = £35.02 (2025 actual) | /offer model | £35.02 | ✅ SOURCE-LINKED | Keep |
|
||||
| 16 | "Competitors producing 10x content" | /offer | No source or evidence | ❌ UNVERIFIED | **Remove or soften** |
|
||||
| 17 | Year 1 cost = £12,400 | /offer model | £4,000 + £500×12 + £200×12 = £12,400 | ✅ DERIVED (arithmetic) | Keep |
|
||||
| 18 | 5.9x Year 1 ROI at 100 custs/mo | /offer model | Depends on 37.3% repeat rate being correct | ⚠️ CONDITIONAL | **Recalculate with verified rate** |
|
||||
| 19 | 2.6 month payback | /offer model | Actual: 3.5 months (first-purchase only) | ❌ WRONG | **Fix math** |
|
||||
| 20 | Revenue peak £1.82M | /offer board summary | £1,820,963 | ✅ SOURCE-LINKED | Keep |
|
||||
| 21 | Revenue 2025 £1.05M | /offer board summary | £1,047,850 | ✅ SOURCE-LINKED | Keep |
|
||||
| 22 | 3,900/year new customers 2025 | /offer board summary | 3,941 | ✅ SOURCE-LINKED | Keep (round to 3,900 is fair) |
|
||||
| 23 | 728,018 validated orders | /offer footer | 728,018 | ✅ SOURCE-LINKED | Keep |
|
||||
|
||||
**Summary: 15/23 claims verified, 4 wrong/unverified, 4 conditional.**
|
||||
|
||||
---
|
||||
|
||||
## Rewritten "Start Today" Block
|
||||
|
||||
### Current (broken):
|
||||
```
|
||||
Ready to Build?
|
||||
If approved, access is provided and build starts immediately.
|
||||
[Approve & Start Build →] ← links to mailto: (empty!)
|
||||
Build begins within 48 hours of approval.
|
||||
```
|
||||
|
||||
### Rewritten:
|
||||
```
|
||||
──────────────────────────────────────
|
||||
YOU'VE SEEN THE DATA. YOU'VE SEEN THE ENGINE.
|
||||
|
||||
Every month without social discovery costs you
|
||||
£60,000+ in lost new-customer revenue.
|
||||
(20,700 fewer new customers × £35.02 AOV = £726K/year lost since 2020)
|
||||
|
||||
THE OFFER:
|
||||
✦ £4,000 one-time build — all 4 pillars
|
||||
✦ £500/month infrastructure — cancel with 30 days' notice
|
||||
✦ Week 4 gate — full review before any ongoing commitment
|
||||
✦ You own everything — server, code, content, data
|
||||
|
||||
RISK REVERSAL:
|
||||
→ If you're not satisfied at Week 4, walk away. No ongoing fees.
|
||||
→ The £4,000 build cost delivers real infrastructure you keep regardless.
|
||||
→ 30-day monthly exit clause. No lock-in. No agency dependency.
|
||||
|
||||
TO START:
|
||||
□ 1. Reply to this email confirming approval
|
||||
□ 2. We'll send Shopify collaborator access request
|
||||
□ 3. 15-min kickoff call within 48 hours
|
||||
□ 4. Infrastructure live by end of Week 1
|
||||
|
||||
[ Book 15-Min Kickoff Call → ] ← Calendly link
|
||||
[ Reply: Approved to Start → ] ← mailto:omair@quikcue.com?subject=...
|
||||
|
||||
Built by Omair @ QuikCue
|
||||
──────────────────────────────────────
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
1. Opens with data-backed cost of inaction (verifiable)
|
||||
2. Offer summarised in 4 bullets (not buried in sections)
|
||||
3. Risk reversal is explicit and bold
|
||||
4. Two CTA options: low-friction (Calendly) + decisive (email)
|
||||
5. Steps are numbered and tiny (3 things, nothing scary)
|
||||
6. Person identified by name
|
||||
|
||||
---
|
||||
|
||||
## Detailed Verification Notes
|
||||
|
||||
### The 97.4% problem
|
||||
The site claims "Organic + Google Ads = 97.4% of all orders." The actual channel breakdown:
|
||||
|
||||
**2025:**
|
||||
| Channel | Orders | Share |
|
||||
|---------|--------|-------|
|
||||
| Organic | 16,942 | 56.6% |
|
||||
| Google Adwords | 8,620 | 28.8% |
|
||||
| Webgains | 2,768 | 9.3% |
|
||||
| Email Newsletter | 1,402 | 4.7% |
|
||||
| Bing | 153 | 0.5% |
|
||||
| Facebook | 34 | 0.1% |
|
||||
| **Total** | **29,919** | **100%** |
|
||||
|
||||
Google + Organic = **85.4%**, not 97.4%. Even adding Bing = 85.9%. Even adding Webgains = 95.2%. None of these groupings produce 97.4%.
|
||||
|
||||
**Recommended reframe:** "85% of orders depend on Google channels. 0.1% come from social. You have zero TikTok, zero Instagram, zero YouTube presence." — This is true AND just as alarming.
|
||||
|
||||
### The 37.3% repeat rate problem
|
||||
No combination of available data produces 37.3%:
|
||||
- Returning orders / total orders = 68.1% (all-time)
|
||||
- Returning orders / total orders = 86.0% (2025)
|
||||
- Average 12-month cohort retention = 57.6%
|
||||
- New customer share = 33.5% (all-time)
|
||||
|
||||
37.3% might have come from a different analysis of the raw jv_data.json before aggregation, but it's not reproducible from the deployed database. The revenue model should use the cohort-verified 57.6% or explain its source.
|
||||
|
||||
### The payback period problem
|
||||
At 100 new customers/month, monthly first-purchase revenue = £3,502.
|
||||
Year 1 cost = £12,400.
|
||||
Payback on first-purchase revenue alone = 12,400 / 3,502 = **3.54 months**, not 2.6.
|
||||
|
||||
The 2.6 month figure likely includes repeat revenue from the first cohorts, which is optimistic for a payback calculation. Standard practice uses first-purchase only.
|
||||
20
CLAUDE.md
@@ -1,20 +0,0 @@
|
||||
# Pi vs CC — Extension Playground
|
||||
|
||||
Pi Coding Agent extension examples and experiments.
|
||||
|
||||
## Tooling
|
||||
- **Package manager**: `bun` (not npm/yarn/pnpm)
|
||||
- **Task runner**: `just` (see justfile)
|
||||
- **Extensions run via**: `pi -e extensions/<name>.ts`
|
||||
|
||||
## Project Structure
|
||||
- `extensions/` — Pi extension source files (.ts)
|
||||
- `specs/` — Feature specifications
|
||||
- `.pi/agents/` — Agent definitions for agent-team extension
|
||||
- `.pi/agent-sessions/` — Ephemeral session files (gitignored)
|
||||
|
||||
## Conventions
|
||||
- Extensions are standalone .ts files loaded by Pi's jiti runtime
|
||||
- Available imports: `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, `@sinclair/typebox`, plus any deps in package.json
|
||||
- Register tools at the top level of the extension function (not inside event handlers)
|
||||
- Use `isToolCallEventType()` for type-safe tool_call event narrowing
|
||||
195
application-answers.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 🎯 Omair Saleh — Full-Stack Engineer Application @ Calvana LTD
|
||||
## The Outlaw Application
|
||||
|
||||
---
|
||||
|
||||
## Field 1: Full Name
|
||||
```
|
||||
Omair Saleh
|
||||
```
|
||||
|
||||
## Field 2: Email Address
|
||||
```
|
||||
omair@quikcue.com
|
||||
```
|
||||
|
||||
## Field 3: LinkedIn / Personal Site / Portfolio
|
||||
```
|
||||
https://www.linkedin.com/in/omair-rescues/
|
||||
```
|
||||
> 💡 If quikcue.com is live, use that. Custom domain > LinkedIn every time.
|
||||
|
||||
## Field 4: GitHub or Equivalent
|
||||
```
|
||||
[Your GitHub URL here]
|
||||
```
|
||||
> 💡 Pin the charity platform, the Hub, and the outreach agent. Let the commit history speak.
|
||||
|
||||
## Field 5: Location
|
||||
```
|
||||
Kuala Lumpur, Malaysia — happy to overlap with London hours. I work when the work needs doing, not when a calendar tells me to.
|
||||
```
|
||||
|
||||
## Field 6: Employment Status
|
||||
> **Select: "Running my own thing"**
|
||||
|
||||
---
|
||||
|
||||
## Field 7: 🔥 "Describe something you built end-to-end"
|
||||
|
||||
```
|
||||
A UK charity came to me with a problem: their donation flow was bleeding donors. Poor conversion, no recurring giving, no peer-to-peer fundraising, no Gift Aid automation. They didn't hand me a spec. There was no spec. There was a problem and a deadline.
|
||||
|
||||
So I built the whole thing. From the database schema to the Stripe webhook handlers.
|
||||
|
||||
Next.js 15 frontend. PostgreSQL with Prisma. Stripe for payments — PaymentIntents for one-off, SetupIntents for recurring. I designed a multi-step checkout with progressive disclosure because I know that every extra field before the payment button is a donor you'll never see again.
|
||||
|
||||
Nobody told me to handle Zakat compliance. I just knew that if a Muslim donor selects Zakat, admin fees need to auto-disable — it's a religious obligation, not a suggestion. So I built it. Nobody told me to move Gift Aid capture to post-payment either. But I knew that asking a donor for their home address BEFORE they've committed to giving is how you kill conversion. So I moved it. HMRC still gets what they need. The charity gets more donations. Problem solved.
|
||||
|
||||
Then I built the P2P fundraising engine — individual pages, team pages, leaderboards, URL-based attribution — architected as its own domain service because I could see it would need to scale independently. Then an admin dashboard. Then a Chatwoot integration for donor support, white-labeled with a Chrome extension I wrote because the dev workflow needed it. Then a data sync pipeline using Playwright to scrape donor CSVs from LaunchGood and reconcile them into Postgres with strict deduplication.
|
||||
|
||||
No PM. No Jira board. No sprint ceremonies. Just me, the problem, and the production environment.
|
||||
|
||||
This is what I do. I see a mess, I build the system, I ship it. In a corporate environment, this gets me in trouble — I've been told I "move too fast", I "don't follow process", I "should wait for alignment." At a startup, this is the only speed that matters.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field 8: Link to Something You've Built
|
||||
```
|
||||
[Link to your charity donation platform or best GitHub repo]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field 9: 🔥 AI/ML API Experience
|
||||
|
||||
```
|
||||
I don't prototype with AI. I ship with it. There's a difference.
|
||||
|
||||
1. AI Outreach Agent: A charity needed to find and contact decision-makers across the entire UK charity sector. Hundreds of thousands of records. I built a Python pipeline that ingests raw Charity Commission data into PostgreSQL, then uses OpenAI to translate natural language queries ("large education charities with income over £1M operating nationally") into SQL filter logic via a custom segment engine. Once leads are qualified, OpenAI generates personalised outreach assets — emails, talking points — based on each charity's actual profile, income band, and sector. Not templated mail-merge garbage. Actually personalised. Then it enriches contacts through Apify to find the CEO, Director, or Head of Fundraising. The whole thing runs from a CLI with deterministic Python scripts underneath — the AI makes decisions, but the infrastructure is boring and reliable. On purpose.
|
||||
|
||||
2. Conversation Intelligence (Hub Platform): Built into a B2B customer service platform. When a support agent opens a Chatwoot conversation, the system pulls the customer's order history from Salla, their previous interactions, and uses OpenAI with structured function calling to suggest contextual responses grounded in real data. Not vibes-based autocomplete — actual responses that reference real order numbers and real product names. I built it this way because I've seen what happens when you let AI hallucinate in customer-facing contexts. It destroys trust instantly.
|
||||
|
||||
3. AI Command Center: This one's borderline unhinged. An autonomous multi-agent system that runs on a 15-minute cron cycle. Reliability agent monitors Sentry. Code-steward reviews MRs on GitLab. Product-driver agent analyses codebase health metrics from Postgres/MySQL and proposes improvements. But — and this is the part that matters — nothing executes without human approval. I built a full safety layer with auto-pause on excessive API spend, command allowlists, and dry-run mode. Because I learned early that autonomous AI without kill switches is just a very expensive way to break production.
|
||||
|
||||
The real lesson across all of these: the API call is the easy part. The hard part is building the deterministic scaffolding that makes AI trustworthy — retry logic, structured outputs, cost ceilings, caching layers, human-in-the-loop gates. Anyone can call OpenAI. I build the systems that make it safe to let OpenAI call the shots.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field 10: Tech Skills Rating
|
||||
|
||||
| Technology | Select This |
|
||||
|---|---|
|
||||
| **React / Next.js** | **production-level experience** |
|
||||
| **Python / Django** | **strong experience** |
|
||||
| **PostgreSQL** | **production-level experience** |
|
||||
| **AWS** | **decent experience** |
|
||||
| **REST API design & integrations** | **production-level experience** |
|
||||
| **OAuth** | **strong experience** |
|
||||
| **CI/CD & Deployment Pipelines** | **strong experience** |
|
||||
| **Docker / containerisation** | **strong experience** |
|
||||
|
||||
> Don't inflate. Let the project descriptions do the talking. Honesty here builds trust for everything else.
|
||||
|
||||
---
|
||||
|
||||
## Field 11: 🔥 "Why does this role interest you specifically?"
|
||||
|
||||
```
|
||||
I'll be honest: I'm a terrible employee.
|
||||
|
||||
Not in the way you'd think. I ship fast, I write clean code, I own my systems end-to-end. But I've learned the hard way that I don't survive in environments where shipping requires three meetings, two approvals, and a Confluence page nobody reads. I've been told I "go rogue." I've been told I "need to wait for the team to align." I've sat in sprint planning sessions thinking about the three features I could've shipped in the time it took to estimate the story points.
|
||||
|
||||
That's not a personality flaw. It's a misallocation.
|
||||
|
||||
Your job post reads like someone wrote it specifically for people like me. "This isn't a role where you'll have a dedicated PM writing specs." Good — I've never needed one. "This isn't a role where 'that's not my job' is a useful phrase." I literally built a Chrome extension because my dev workflow for a Chatwoot integration was annoying me. Nobody asked me to. The friction existed, so I killed it.
|
||||
|
||||
But here's what actually made me stop scrolling and pay attention:
|
||||
|
||||
You have cash, audience, distribution, and PMF. You DON'T have engineers. That's the most dangerous inflection point for a startup — the gap between "this works" and "this scales." That gap gets filled by someone who can pick up an entire problem, architect a solution, ship it as a microservice, and move on to the next one without waiting for permission. I've been doing exactly that for the past year: a full donation platform with Stripe, P2P, and Gift Aid compliance. A multi-service B2B operations hub with 30+ services, AI automation, and real-time event processing. An outreach engine that processes hundreds of thousands of leads with AI. All end-to-end. All without a PM.
|
||||
|
||||
Your stack is my stack — Next.js, Python, PostgreSQL, Stripe, OAuth, Docker. Your AI ambitions are things I've already built. Your microservices architecture is how I think.
|
||||
|
||||
I watched Charlie's Loom. "We're going to the moon with this thing." I believe it. And I know that the difference between going to the moon and talking about going to the moon is having someone in the engine room who builds without asking for permission.
|
||||
|
||||
That's me. I'm the guy in the engine room.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field 12: Salary Expectation
|
||||
```
|
||||
£50,000–£65,000 GBP/year — flexible on structure. If the equity conversation is real, I'm more interested in upside than ceiling.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field 13: How soon could you start?
|
||||
> **Select: "Immediately"** — you're running your own thing, you set your own timeline.
|
||||
|
||||
---
|
||||
|
||||
## Field 14: 🔥 Loom Video Script (THE KNOCKOUT PUNCH)
|
||||
|
||||
```
|
||||
[0:00-0:20]
|
||||
"Hey Charlie — I'm Omair. I'll be straight with you: I'm a terrible fit
|
||||
for most companies. I've been told I move too fast, I don't wait for
|
||||
alignment, I build things nobody asked for. Turns out those are
|
||||
features, not bugs — just depends on the environment. Your job post
|
||||
reads like it was written for someone exactly like me."
|
||||
|
||||
[0:20-0:55] [SCREEN SHARE: Charity donation platform]
|
||||
"Quick example. A UK charity had a broken donation flow. No spec, no PM,
|
||||
no Jira. Just a problem. So I built this — end to end. Next.js 15,
|
||||
Prisma, PostgreSQL, Stripe. Multi-step checkout, recurring giving, P2P
|
||||
fundraising, Zakat compliance, Gift Aid for HMRC. Designed the schema,
|
||||
wrote the webhook handlers, deployed it. That's how I work — give me the
|
||||
problem, get out of the way."
|
||||
|
||||
[0:55-1:25] [SCREEN SHARE: Hub platform or AI outreach agent]
|
||||
"Then there's this — an AI outreach engine I built. Ingests hundreds of
|
||||
thousands of charity records, uses OpenAI to segment and qualify leads,
|
||||
generates personalised outreach. The AI is wrapped in deterministic
|
||||
Python with cost controls and approval gates — because I've learned that
|
||||
AI without guardrails is just an expensive way to break things."
|
||||
|
||||
[1:25-1:50]
|
||||
"Your post said 'we have cash, audience, distribution, and PMF — we just
|
||||
need YOU.' I felt that. I've spent the last year building entire systems
|
||||
solo — the donation platform, a B2B SaaS hub with 30+ microservices, AI
|
||||
agents running on cron cycles. No PM, no sprint ceremonies. Just
|
||||
problems and production. That's the only way I know how to work — and
|
||||
it sounds like that's exactly what you need."
|
||||
|
||||
[1:50-2:00]
|
||||
"I don't need onboarding. I need a problem and a git repo. Let's talk."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ PRE-SUBMIT CHECKLIST
|
||||
|
||||
- [ ] GitHub pinned repos updated and READMEs are clean
|
||||
- [ ] LinkedIn headline: "Full-Stack Engineer | I build things nobody asked for"
|
||||
- [ ] All answers proofread — raw ≠ sloppy
|
||||
- [ ] Loom recorded — show real projects, show real energy, close hard
|
||||
- [ ] quikcue.com email shows you're a founder, not an applicant
|
||||
|
||||
---
|
||||
|
||||
## 🎭 THE OUTLAW POSITIONING — WHY THIS WORKS
|
||||
|
||||
The entire job posting is a filter for people who **can't survive in corporate**:
|
||||
|
||||
| What Their Post Says | What It Actually Means | Your Outlaw Angle |
|
||||
|---|---|---|
|
||||
| "No PM writing specs for you" | We need self-starters | "I've never needed a PM. I AM the PM." |
|
||||
| "Not just one part of the codebase" | Generalists only | "I built frontend, backend, infra, Chrome extensions, data pipelines — in one project." |
|
||||
| "'That's not my job' isn't useful" | Ego-free builders | "I built a Chrome extension because a workflow annoyed me. Nobody asked." |
|
||||
| "Ambiguity of early-stage work" | Chaos tolerance required | "Chaos is where I do my best work. Structure is where I suffocate." |
|
||||
| "No AI screening — we read every app" | Charlie reads this personally | You're speaking directly to a founder. Be human. Be direct. |
|
||||
|
||||
**The core message in every answer:** *The things that make me a liability in corporate make me your most valuable hire. I don't wait for permission. I don't need process. I see problems and I ship solutions. That's why big companies don't know what to do with me — and it's exactly why you should.*
|
||||
BIN
audit-demos.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
audit-footer.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
audit-hero.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
audit-offer-bottom.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
audit-offer-mid.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
audit-offer-mid2.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
audit-offer-top.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
audit-proposal-top.png
Normal file
|
After Width: | Height: | Size: 708 KiB |
BIN
auth-login.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
auth-success.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
auth-wrong.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
91
bun.lock
@@ -5,24 +5,115 @@
|
||||
"": {
|
||||
"name": "pi-vs-cc",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.78.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"yaml": "^2.8.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/cli": "^0.1.1",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.78.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@playwright/cli": ["@playwright/cli@0.1.1", "", { "dependencies": { "minimist": "^1.2.5", "playwright": "1.59.0-alpha-1771104257000" }, "bin": { "playwright-cli": "playwright-cli.js" } }, "sha512-9k11ZfDwAfMVDDIuEVW1Wvs8SoDNXIY1dNQ+9C9/SS8ZmElkcxesu5eoL7vNa96ntibUGaq1TM2qQoqvdl/I9g=="],
|
||||
|
||||
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||
|
||||
"node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"playwright": ["playwright@1.59.0-alpha-1771104257000", "", { "dependencies": { "playwright-core": "1.59.0-alpha-1771104257000" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-6SCMMMJaDRsSqiKVLmb2nhtLES7iTYawTWWrQK6UdIGNzXi8lka4sLKRec3L4DnTWwddAvCuRn8035dhNiHzbg=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.59.0-alpha-1771104257000", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-YiXup3pnpQUCBMSIW5zx8CErwRx4K6O5Kojkw2BzJui8MazoMUDU6E3xGsb1kzFviEAE09LFQ+y1a0RhIJQ5SA=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||
|
||||
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||
|
||||
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
}
|
||||
}
|
||||
|
||||
12
calvana-build/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "calvana",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"pg": "^8.13.0"
|
||||
}
|
||||
}
|
||||
153
calvana-build/server/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'dokploy-postgres',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
user: process.env.DB_USER || 'dokploy',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'calvana',
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
await pool.query('SELECT 1');
|
||||
res.json({ status: 'ok', db: 'connected' });
|
||||
} catch (e) {
|
||||
res.status(500).json({ status: 'error', db: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET ships
|
||||
app.get('/api/ships', async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM ships ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST ship
|
||||
app.post('/api/ships', async (req, res) => {
|
||||
const { title, status, metric, details } = req.body;
|
||||
if (!title) return res.status(400).json({ error: 'title required' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO ships (title, status, metric, details) VALUES ($1, $2, $3, $4) RETURNING *',
|
||||
[title, status || 'planned', metric || null, details || null]
|
||||
);
|
||||
res.status(201).json(rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH ship
|
||||
app.patch('/api/ships/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { title, status, metric, details } = req.body;
|
||||
try {
|
||||
const sets = [];
|
||||
const vals = [];
|
||||
let i = 1;
|
||||
if (title !== undefined) { sets.push(`title=$${i++}`); vals.push(title); }
|
||||
if (status !== undefined) { sets.push(`status=$${i++}`); vals.push(status); }
|
||||
if (metric !== undefined) { sets.push(`metric=$${i++}`); vals.push(metric); }
|
||||
if (details !== undefined) { sets.push(`details=$${i++}`); vals.push(details); }
|
||||
if (sets.length === 0) return res.status(400).json({ error: 'nothing to update' });
|
||||
sets.push(`updated_at=NOW()`);
|
||||
vals.push(id);
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE ships SET ${sets.join(', ')} WHERE id=$${i} RETURNING *`,
|
||||
vals
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE ship
|
||||
app.delete('/api/ships/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'DELETE FROM ships WHERE id=$1 RETURNING *',
|
||||
[id]
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ deleted: true, ship: rows[0] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE oops
|
||||
app.delete('/api/oops/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'DELETE FROM oops WHERE id=$1 RETURNING *',
|
||||
[id]
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ deleted: true, oops: rows[0] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET oops
|
||||
app.get('/api/oops', async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM oops ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST oops
|
||||
app.post('/api/oops', async (req, res) => {
|
||||
const { description, fix_time, commit_link } = req.body;
|
||||
if (!description) return res.status(400).json({ error: 'description required' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO oops (description, fix_time, commit_link) VALUES ($1, $2, $3) RETURNING *',
|
||||
[description, fix_time || null, commit_link || null]
|
||||
);
|
||||
res.status(201).json(rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static(path.join(__dirname, '..', 'html')));
|
||||
|
||||
// SPA fallback — serve index.html for unmatched routes
|
||||
app.get('*', (req, res) => {
|
||||
// Check if requesting a known page directory
|
||||
const pagePath = path.join(__dirname, '..', 'html', req.path, 'index.html');
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(pagePath)) {
|
||||
return res.sendFile(pagePath);
|
||||
}
|
||||
res.sendFile(path.join(__dirname, '..', 'html', 'index.html'));
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 80;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Calvana server listening on :${PORT}`);
|
||||
});
|
||||
BIN
data/agents.db
Normal file
BIN
data/agents.db-shm
Normal file
BIN
data/agents.db-wal
Normal file
54
data/bot.log
Normal file
@@ -0,0 +1,54 @@
|
||||
🤖 Telegram Agent Orchestrator starting...
|
||||
Polling for updates...
|
||||
The system cannot find the path specified.
|
||||
The system cannot find the path specified.
|
||||
[agent 1] error: 50 | }
|
||||
51 | if (status === 422) {
|
||||
52 | return new UnprocessableEntityError(status, error, message, headers);
|
||||
53 | }
|
||||
54 | if (status === 429) {
|
||||
55 | return new RateLimitError(status, error, message, headers);
|
||||
^
|
||||
error: 429 {"type":"error","error":{"type":"rate_limit_error","message":"This request would exceed your organization's rate limit of 10,000 input tokens per minute (org: 4362d07d-8082-4159-b447-7c9f0172030e, model: claude-sonnet-4-20250514). For details, refer to: https://docs.claude.com/en/api/rate-limits. You can see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."},"request_id":"req_011CYeuG3V1tiXgYoBie69XF"}
|
||||
status: 429,
|
||||
headers: Headers {
|
||||
"date": "Mon, 02 Mar 2026 20:41:30 GMT",
|
||||
"content-type": "application/json",
|
||||
"transfer-encoding": "chunked",
|
||||
"connection": "keep-alive",
|
||||
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
|
||||
"content-encoding": "gzip",
|
||||
"vary": "Accept-Encoding",
|
||||
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
|
||||
"x-should-retry": "true",
|
||||
"anthropic-ratelimit-input-tokens-limit": "10000",
|
||||
"anthropic-ratelimit-input-tokens-remaining": "0",
|
||||
"anthropic-ratelimit-input-tokens-reset": "2026-03-02T20:42:31Z",
|
||||
"anthropic-ratelimit-output-tokens-limit": "4000",
|
||||
"anthropic-ratelimit-output-tokens-remaining": "4000",
|
||||
"anthropic-ratelimit-output-tokens-reset": "2026-03-02T20:41:30Z",
|
||||
"anthropic-ratelimit-requests-limit": "5",
|
||||
"anthropic-ratelimit-requests-remaining": "0",
|
||||
"anthropic-ratelimit-requests-reset": "2026-03-02T20:42:28Z",
|
||||
"retry-after": "11",
|
||||
"anthropic-ratelimit-tokens-limit": "14000",
|
||||
"anthropic-ratelimit-tokens-remaining": "4000",
|
||||
"anthropic-ratelimit-tokens-reset": "2026-03-02T20:41:30Z",
|
||||
"request-id": "req_011CYeuG3V1tiXgYoBie69XF",
|
||||
"anthropic-organization-id": "4362d07d-8082-4159-b447-7c9f0172030e",
|
||||
"server": "cloudflare",
|
||||
"x-envoy-upstream-service-time": "1023",
|
||||
"cf-cache-status": "DYNAMIC",
|
||||
"x-robots-tag": "none",
|
||||
"cf-ray": "9d6338f76a681ec5-KUL",
|
||||
},
|
||||
requestID: "req_011CYeuG3V1tiXgYoBie69XF",
|
||||
error: {
|
||||
type: "error",
|
||||
error: [Object ...],
|
||||
request_id: "req_011CYeuG3V1tiXgYoBie69XF",
|
||||
},
|
||||
|
||||
at generate (C:\Users\uldvs\OneDrive\Desktop\work\pi-agent-improved-main\node_modules\@anthropic-ai\sdk\core\error.mjs:55:20)
|
||||
at makeRequest (C:\Users\uldvs\OneDrive\Desktop\work\pi-agent-improved-main\node_modules\@anthropic-ai\sdk\client.mjs:309:30)
|
||||
|
||||
BIN
debug-2550.png
Normal file
|
After Width: | Height: | Size: 422 KiB |
BIN
final-beforeafter.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
final-dashboard-2020.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
final-dashboard.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
final-hero.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
final-offer-builtby.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
fix-hero.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
fix-offer-channel.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
fix-offer-cta.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
fix-offer-hero.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
fix-offer-model.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
88
gen_hero.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Generate a world-class hero image for Pledge Now, Pay Later."""
|
||||
import sys, io, os, time
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from PIL import Image
|
||||
|
||||
client = genai.Client(api_key="AIzaSyCHnesXLjPw-UgeZaQotut66bgjFdvy12E")
|
||||
MODEL = "gemini-3-pro-image-preview"
|
||||
|
||||
OUT_DIR = "pledge-now-pay-later/public/images/landing"
|
||||
BRAND_DIR = "pledge-now-pay-later/brand/photography"
|
||||
|
||||
PROMPTS = [
|
||||
# Concept 1: Phone notification at gala
|
||||
"""Photorealistic close-up documentary photograph.
|
||||
A woman's hand holding a smartphone at a charity gala dinner table. The phone screen glows bright showing a green checkmark payment confirmation notification. Her hand is in sharp focus.
|
||||
Background: beautifully soft bokeh of warm golden tungsten chandelier lights, round dinner tables with white tablecloths, blurred guests in formal attire. A glass of water and edge of a plate visible on the dark wooden table.
|
||||
British South Asian woman, dark navy blazer sleeve, simple gold bracelet on wrist.
|
||||
Shot on Sony A7IV, 50mm f/1.4, available warm tungsten light. Extremely shallow depth of field. Documentary candid style, warm color temperature.
|
||||
The mood: quiet triumph. The pledge came through. Money in the bank.
|
||||
Portrait orientation, 4:5 aspect ratio. Professional editorial photography.""",
|
||||
|
||||
# Concept 2: Dashboard laptop at desk after event
|
||||
"""Photorealistic documentary photograph of a charity manager's desk, end of a successful fundraising evening.
|
||||
An open MacBook showing a dashboard with bright green progress bars at 100 percent and payment confirmations. The laptop screen glows in the dim warm light. A phone beside it shows a WhatsApp message. A cup of tea, reading glasses folded on the desk.
|
||||
The setting is a quiet office after an event. Warm desk lamp casting golden light, a window showing evening London skyline with city lights in the far background, completely out of focus.
|
||||
British South Asian woman in her 40s, slight smile, looking at the laptop screen, only her silhouette partially visible from the side. Not looking at camera.
|
||||
Shot on Leica Q2, 28mm f/1.7, available warm lamp light and blue window light. Shallow depth of field. Documentary candid style.
|
||||
The mood: satisfied relief. Every pledge tracked. Every penny accounted for.
|
||||
Portrait orientation, 4:5 aspect ratio. Professional editorial photography.""",
|
||||
|
||||
# Concept 3: The green glow moment
|
||||
"""Photorealistic documentary photograph capturing the exact moment of success.
|
||||
A close-up of a smartphone in a woman's hand, the screen casting a soft green glow on her face from below. She is standing at the edge of a busy charity gala ballroom. The phone shows a payment dashboard with multiple green indicators.
|
||||
Background: a sweeping view of a London hotel ballroom with crystal chandeliers creating beautiful warm bokeh circles. Guests at round tables, energy and generosity in the air. All beautifully blurred.
|
||||
British woman wearing a dark structured blazer, hijab, professional. She holds the phone at mid-chest level, glancing down at it with a subtle knowing expression. Candid, not posed.
|
||||
Shot on Canon R5, 85mm f/1.2, warm tungsten available light. Extremely shallow depth of field. The phone and her nearest hand are razor sharp, face slightly soft, background completely dissolved into warm golden bokeh circles.
|
||||
Cinematic documentary photography. The feeling: this is what success looks like. Quiet. Precise. Money in the bank.
|
||||
Portrait orientation, 4:5 aspect ratio. Professional editorial photography.""",
|
||||
]
|
||||
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
os.makedirs(BRAND_DIR, exist_ok=True)
|
||||
|
||||
results = []
|
||||
for i, prompt in enumerate(PROMPTS):
|
||||
for attempt in range(3):
|
||||
try:
|
||||
print(f"\n--- Generating concept {i+1}/3 (attempt {attempt+1}) ---")
|
||||
response = client.models.generate_content(
|
||||
model=MODEL,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
temperature=1.0,
|
||||
),
|
||||
)
|
||||
|
||||
found = False
|
||||
for part in response.candidates[0].content.parts:
|
||||
if part.inline_data and part.inline_data.mime_type.startswith("image/"):
|
||||
img = Image.open(io.BytesIO(part.inline_data.data))
|
||||
fname = f"hero-concept-{i+1}.jpg"
|
||||
path = os.path.join(OUT_DIR, fname)
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
img.save(path, "JPEG", quality=92, optimize=True)
|
||||
sz = os.path.getsize(path)
|
||||
print(f" OK {fname} -- {img.size[0]}x{img.size[1]}, {sz//1024}KB")
|
||||
results.append((fname, img.size, sz))
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
else:
|
||||
print(" No image in response, retrying...")
|
||||
|
||||
except Exception as e:
|
||||
emsg = str(e).encode('ascii', 'replace').decode('ascii')
|
||||
print(f" Error: {emsg}")
|
||||
if attempt < 2:
|
||||
time.sleep(5)
|
||||
|
||||
print(f"\n=== Generated {len(results)}/3 hero concepts ===")
|
||||
for fname, size, sz in results:
|
||||
print(f" {fname}: {size[0]}x{size[1]}, {sz//1024}KB")
|
||||
144
gen_kaffarah.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Generate 3 on-brand kaffarah-community.jpg replacements.
|
||||
Uses anti-AI prompting strategy from cr-brand-style.json.
|
||||
"""
|
||||
import sys, io, os, time, json
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from PIL import Image
|
||||
|
||||
API_KEY = "AIzaSyCHnesXLjPw-UgeZaQotut66bgjFdvy12E"
|
||||
MODEL = "gemini-3-pro-image-preview"
|
||||
OUT = "screenshots/kaffarah-replacements"
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
client = genai.Client(api_key=API_KEY)
|
||||
|
||||
# ── ANTI-AI STYLE BLOCK ──
|
||||
# No camera names, no "golden hour", no emotion words, no "bokeh".
|
||||
# Describe PHYSICS of light, SPECIFIC objects, ACTIONS not feelings.
|
||||
|
||||
STYLE = (
|
||||
"Raw documentary photograph with visible film grain and slight color noise in the shadows. "
|
||||
"The color is MUTED — pulled-back saturation, faded dusty quality, blacks slightly lifted, "
|
||||
"NOT warm amber or orange-tinted. Highlights have a faint yellow-green cast. "
|
||||
"Shadows lean cool and neutral. "
|
||||
"Single hard directional light source creating deep shadows on one side. "
|
||||
"Unlit areas stay DARK, not filled in. "
|
||||
"The framing is IMPERFECT — a partial figure or object intrudes at one edge of the frame. "
|
||||
"The subject is slightly off-center. There is foreground obstruction. "
|
||||
"Skin has visible pores, uneven tone, slight dust. Hair is uncombed. "
|
||||
"Clothing is thin worn cotton, faded, creased, with visible stitching. "
|
||||
"The environment is CLUTTERED with real objects at multiple depth planes. "
|
||||
"NO smooth skin. NO perfect composition. NO warm glow. NO clean backgrounds. "
|
||||
"Aspect ratio: exactly 2:1 wide landscape. "
|
||||
)
|
||||
|
||||
PROMPTS = {
|
||||
"kaffarah-v1.jpg": (
|
||||
STYLE +
|
||||
"Three children sit on a swept-dirt floor against a crumbling plastered wall eating from "
|
||||
"shared metal thali plates. A boy around 8 is mid-chew, mouth slightly open, looking "
|
||||
"sideways at something outside the frame. His faded blue cotton kurta has a torn collar. "
|
||||
"Next to him a younger girl scoops rice with her right hand, not looking up. Her hair is "
|
||||
"tangled and hasn't been brushed. Behind them, a wooden charpai with sagging rope webbing, "
|
||||
"a plastic water jug, and a torn calendar hanging crooked on the wall. Afternoon sun comes "
|
||||
"through a doorway on the left, casting a hard beam across the floor — the far wall stays "
|
||||
"in deep shadow. Someone's bare foot and ankle are visible at the bottom-right edge of the "
|
||||
"frame, cropped off. Dust motes visible in the light beam. The floor has a cracked cement "
|
||||
"patch and a worn woven mat. Muted desaturated color with visible grain."
|
||||
),
|
||||
|
||||
"kaffarah-v2.jpg": (
|
||||
STYLE +
|
||||
"A boy around 6 sits at a rough wooden bench eating rice and dal from a dented steel plate. "
|
||||
"His hand is in the food, fingers pressing rice together. He is looking down at his plate, "
|
||||
"not at the camera. His dark hair sticks up on one side where he slept on it. He wears a "
|
||||
"faded brown cotton shirt buttoned wrong — one side hangs lower than the other. His "
|
||||
"fingernails have dirt under them. The bench has deep scratches and a water ring stain. "
|
||||
"Behind him, a plastered wall with a long crack running diagonally, a nail with nothing "
|
||||
"on it, and a small high window letting in a hard shaft of light from the right. Two "
|
||||
"other children are visible in the mid-ground, slightly out of focus, also eating. "
|
||||
"An adult's elbow and forearm in a grey cotton sleeve intrudes into the left edge of "
|
||||
"frame. On the bench next to the boy: a scratched steel cup with water. The light "
|
||||
"illuminates only the right side of his face. The left side falls into shadow. "
|
||||
"Muted color, visible grain, slight chromatic aberration at contrast edges."
|
||||
),
|
||||
|
||||
"kaffarah-v3.jpg": (
|
||||
STYLE +
|
||||
"Seen from slightly behind and to the side of a woman in a faded cream dupatta who is "
|
||||
"setting down a metal plate of food in front of a small child seated on a woven mat. "
|
||||
"We see the woman's hands and forearms — veins visible, a thin glass bangle on one wrist. "
|
||||
"The child, about 5, reaches for the plate with both small hands. The child's face is in "
|
||||
"three-quarter profile, slightly blurred because the focus is on the hands and the plate. "
|
||||
"The food is simple — a mound of rice, yellow dal, a piece of flatbread folded on the side. "
|
||||
"The mat has fraying edges and a cigarette burn mark. Against the wall behind them: a "
|
||||
"stacked row of steel plates, a plastic bag hanging on a nail, peeling turquoise paint "
|
||||
"revealing brown plaster underneath. Hard afternoon light from a window on the right. "
|
||||
"Another child sits further back, eating, almost lost in the dim background. A power "
|
||||
"cable runs along the top of the wall. Muted, slightly desaturated, dusty color. "
|
||||
"Fine grain throughout. This is a REAL moment, not posed."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def generate_and_save(filename, prompt, max_retries=3):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
t0 = time.time()
|
||||
print(f" [{attempt+1}/{max_retries}] {filename}...")
|
||||
|
||||
resp = client.models.generate_content(
|
||||
model=MODEL,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
temperature=1.0,
|
||||
),
|
||||
)
|
||||
|
||||
for part in resp.candidates[0].content.parts:
|
||||
if part.inline_data and part.inline_data.mime_type.startswith("image/"):
|
||||
img = Image.open(io.BytesIO(part.inline_data.data))
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
|
||||
path = os.path.join(OUT, filename)
|
||||
|
||||
# Compress to stay under 2 MB
|
||||
for q in [88, 82, 75, 65]:
|
||||
img.save(path, "JPEG", quality=q, optimize=True, progressive=True)
|
||||
if os.path.getsize(path) < 2_000_000:
|
||||
break
|
||||
|
||||
sz = os.path.getsize(path)
|
||||
print(f" [OK] {filename} -- {img.width}x{img.height}, {sz//1024} KB, {time.time()-t0:.1f}s")
|
||||
return True
|
||||
|
||||
print(f" [WARN] no image in response")
|
||||
|
||||
except Exception as e:
|
||||
print(f" [ERR] {str(e)[:200]}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(5 * (attempt + 1))
|
||||
|
||||
print(f" [FAIL] {filename}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Generating {len(PROMPTS)} kaffarah replacements -> {os.path.abspath(OUT)}\n")
|
||||
|
||||
ok = 0
|
||||
for fn, prompt in PROMPTS.items():
|
||||
if generate_and_save(fn, prompt):
|
||||
ok += 1
|
||||
time.sleep(2)
|
||||
|
||||
print(f"\nDone: {ok}/{len(PROMPTS)}")
|
||||
for f in sorted(os.listdir(OUT)):
|
||||
sz = os.path.getsize(os.path.join(OUT, f))
|
||||
print(f" {f} -- {sz//1024} KB")
|
||||
84
gen_personas.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Generate 4 persona images for the landing page gap-px grid.
|
||||
All landscape 3:2, documentary candid, consistent warm tone.
|
||||
Uses gemini-3-pro-image-preview (Nano Banana Pro).
|
||||
"""
|
||||
import os, time, sys
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
client = genai.Client(api_key="AIzaSyCHnesXLjPw-UgeZaQotut66bgjFdvy12E")
|
||||
OUT = "pledge-now-pay-later/public/images/landing"
|
||||
|
||||
PROMPTS = {
|
||||
"persona-charity-manager.jpg": (
|
||||
"A British South Asian woman in her late 40s wearing a navy cardigan and simple hijab, "
|
||||
"sitting at a desk in a mosque community room. She is looking down at a laptop screen, focused, "
|
||||
"one hand on the trackpad. Warm tungsten overhead lights. A prayer timetable is pinned to a "
|
||||
"corkboard on the wall behind her. Stacks of folders and a mug of tea on the desk. "
|
||||
"Shot on Canon EOS R5, 50mm, f/2.0, available light. Documentary candid, not looking at camera. "
|
||||
"Warm, grounded, purposeful. 3:2 landscape aspect ratio."
|
||||
),
|
||||
"persona-programme-manager.jpg": (
|
||||
"A British Arab man in his mid-30s wearing a smart navy polo shirt, sitting alone at a desk "
|
||||
"in a modern charity office. He has a laptop open and a printed spreadsheet with highlighted rows "
|
||||
"beside it. He is writing notes in a Moleskine notebook, pen in hand, concentrating. "
|
||||
"Natural window light from the left, soft shadows. A monitor showing a campaign calendar is "
|
||||
"blurred in the background. Clean desk, professional but not corporate. "
|
||||
"Shot on Canon EOS R5, 35mm, f/1.8, available light. Documentary candid. "
|
||||
"Organised, calm authority. 3:2 landscape aspect ratio."
|
||||
),
|
||||
"persona-fundraiser.jpg": (
|
||||
"A young British Black woman in her mid-20s sitting on a bench in a London park, looking at her "
|
||||
"phone screen. She wears a casual olive utility jacket and has a tote bag beside her. "
|
||||
"Overcast British daylight, soft diffused light. Shallow depth of field — bare winter trees and "
|
||||
"a path blurred behind her. She looks focused and slightly pleased at something on the screen. "
|
||||
"Shot on Sony A7III, 85mm, f/1.4, natural light. Documentary street photography. "
|
||||
"Independent, resourceful. 3:2 landscape aspect ratio."
|
||||
),
|
||||
"persona-volunteer.jpg": (
|
||||
"A young British South Asian man in his early 20s wearing a lanyard and a plain dark polo shirt, "
|
||||
"leaning forward at a round table during a charity dinner gala. He is handing a small card "
|
||||
"to a seated older woman across the table. Warm gala tungsten lighting, white tablecloths, "
|
||||
"bokeh chandeliers in the background. Other guests are blurred but visible at adjacent tables. "
|
||||
"Shot on Canon EOS R5, 50mm, f/1.8, available light. Documentary candid event photography. "
|
||||
"Energetic, helpful. 3:2 landscape aspect ratio."
|
||||
),
|
||||
}
|
||||
|
||||
def generate(filename, prompt):
|
||||
t0 = time.time()
|
||||
print(f" [GEN] {filename}...")
|
||||
try:
|
||||
resp = client.models.generate_content(
|
||||
model="gemini-3-pro-image-preview",
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["TEXT", "IMAGE"],
|
||||
),
|
||||
)
|
||||
for part in resp.candidates[0].content.parts:
|
||||
if part.inline_data:
|
||||
path = os.path.join(OUT, filename)
|
||||
with open(path, "wb") as f:
|
||||
f.write(part.inline_data.data)
|
||||
sz = os.path.getsize(path) / 1024
|
||||
print(f" [OK] {filename} -- {sz:.0f}KB ({time.time()-t0:.1f}s)")
|
||||
return filename, True
|
||||
print(f" [FAIL] {filename} -- no image in response")
|
||||
return filename, False
|
||||
except Exception as e:
|
||||
print(f" [FAIL] {filename} -- {e}")
|
||||
return filename, False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Generating {len(PROMPTS)} persona images...")
|
||||
ok, fail = 0, 0
|
||||
# Generate 2 at a time (rate limits)
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
futures = {pool.submit(generate, fn, p): fn for fn, p in PROMPTS.items()}
|
||||
for f in as_completed(futures):
|
||||
_, success = f.result()
|
||||
if success: ok += 1
|
||||
else: fail += 1
|
||||
print(f"\nDone: {ok} ok, {fail} failed")
|
||||
84
gen_personas_v2.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Generate 4 cinematic persona images — moment shots, not portraits.
|
||||
Wide 2:1 aspect ratio for editorial strip layout.
|
||||
"""
|
||||
import os, time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
client = genai.Client(api_key="AIzaSyCHnesXLjPw-UgeZaQotut66bgjFdvy12E")
|
||||
OUT = "pledge-now-pay-later/public/images/landing"
|
||||
|
||||
PROMPTS = {
|
||||
"persona-01-dinner.jpg": (
|
||||
"End of a charity fundraising dinner. A round banquet table, white tablecloth slightly rumpled. "
|
||||
"A single small pledge card sits slightly askew on the tablecloth near an empty water glass. "
|
||||
"Chairs pushed back. Warm tungsten chandelier light creates golden glow. A few guests leaving "
|
||||
"in the far background, blurred. Crumpled napkin. The generosity happened here, but the table "
|
||||
"is emptying. Cinematic wide shot, melancholy but beautiful. No one looking at camera. "
|
||||
"Shot on Leica Q2, 28mm, f/1.7, available light. Portra-like color rendering. "
|
||||
"2:1 landscape aspect ratio."
|
||||
),
|
||||
"persona-02-phone.jpg": (
|
||||
"Close-up of a British South Asian woman's hands holding a smartphone above a wooden kitchen table. "
|
||||
"The phone screen shows a messaging app with a green chat bubble. Warm afternoon window light "
|
||||
"from the left illuminates the scene. Shallow depth of field - the rest of the table (a mug, "
|
||||
"a notebook) is softly blurred. Her sleeves are pushed up casually. Intimate, everyday, relatable. "
|
||||
"Not looking at camera - we only see hands and phone. "
|
||||
"Shot on Sony A7III, 55mm, f/1.4, natural light. Warm, honest. "
|
||||
"2:1 landscape aspect ratio."
|
||||
),
|
||||
"persona-03-volunteer.jpg": (
|
||||
"A young British South Asian man with a lanyard and dark polo shirt, seen from behind and slightly "
|
||||
"to the side, walking between round dinner tables at a charity gala. He carries a small stack of "
|
||||
"cards in one hand. Seated guests in smart dress at the white-clothed tables. Warm golden gala "
|
||||
"lighting, crystal chandelier bokeh in the upper background. The volunteer is in motion, purposeful. "
|
||||
"Not looking at camera. Other volunteers visible but blurred. Energy, purpose, youth. "
|
||||
"Shot on Canon EOS R5, 35mm, f/1.8, available light. Documentary candid event photography. "
|
||||
"2:1 landscape aspect ratio."
|
||||
),
|
||||
"persona-04-desk.jpg": (
|
||||
"A clean wooden desk photographed from a 45-degree overhead angle. An open laptop shows a "
|
||||
"spreadsheet with organized rows and a green progress indicator. Beside the laptop: a neat stack "
|
||||
"of printed A4 papers with a black binder clip, a fine-point pen, and a ceramic mug of tea. "
|
||||
"Soft morning window light from the left creates gentle shadows. No person visible - just the "
|
||||
"workspace. Everything is orderly, calm, under control. Documentary still life. "
|
||||
"Shot on Fujifilm GFX 50S, 63mm, f/4.0, natural light. Calm, precise. "
|
||||
"2:1 landscape aspect ratio."
|
||||
),
|
||||
}
|
||||
|
||||
def generate(filename, prompt):
|
||||
t0 = time.time()
|
||||
print(f" [GEN] {filename}...")
|
||||
try:
|
||||
resp = client.models.generate_content(
|
||||
model="gemini-3-pro-image-preview",
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["TEXT", "IMAGE"],
|
||||
),
|
||||
)
|
||||
for part in resp.candidates[0].content.parts:
|
||||
if part.inline_data:
|
||||
path = os.path.join(OUT, filename)
|
||||
with open(path, "wb") as f:
|
||||
f.write(part.inline_data.data)
|
||||
sz = os.path.getsize(path) / 1024
|
||||
print(f" [OK] {filename} -- {sz:.0f}KB ({time.time()-t0:.1f}s)")
|
||||
return filename, True
|
||||
print(f" [FAIL] {filename} -- no image in response")
|
||||
return filename, False
|
||||
except Exception as e:
|
||||
print(f" [FAIL] {filename} -- {e}")
|
||||
return filename, False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Generating {len(PROMPTS)} cinematic persona images...")
|
||||
ok = 0
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
futures = {pool.submit(generate, fn, p): fn for fn, p in PROMPTS.items()}
|
||||
for f in as_completed(futures):
|
||||
_, success = f.result()
|
||||
if success: ok += 1
|
||||
print(f"\nDone: {ok}/{len(PROMPTS)} ok")
|
||||
229
job-application-guide.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# 🎯 Killer Application Guide — Full-Stack Engineer @ Calvana LTD
|
||||
|
||||
---
|
||||
|
||||
## 📋 JOB REVIEW SUMMARY
|
||||
|
||||
### Company: Calvana LTD
|
||||
- **B2B SaaS startup** solving client acquisition for B2B companies
|
||||
- Starting by dominating the "internet marketing" agency & coaching market
|
||||
- Claims: cash ✅, audience ✅, distribution ✅, product-market fit ✅
|
||||
- Looking for **first engineering hires** — massive ownership opportunity
|
||||
- Has a Loom video: https://www.loom.com/share/1e6f7f6255d74e7785a7a8e48c2d5788
|
||||
- $2,000 referral bonus signals they're actively hunting
|
||||
|
||||
### Role: Full-Stack Engineer (Early Hire)
|
||||
- **Location:** Remote (ideally London timezone)
|
||||
- **Type:** Full-time
|
||||
- **Stack:** Next.js (frontend) + Django/PostgreSQL (backend) + Pulumi/AWS (infra)
|
||||
- **Nature:** End-to-end ownership, microservices, AI-powered features, 3rd-party API integrations
|
||||
|
||||
### 🔑 What They REALLY Want (Reading Between the Lines)
|
||||
1. **A builder, not an employee** — someone who acts like a co-founder
|
||||
2. **Self-directed** — no PM, no Figma specs, no hand-holding
|
||||
3. **Speed over perfection** — ship fast, iterate, "high velocity"
|
||||
4. **AI-native** — not just curious, but has actually BUILT with AI APIs
|
||||
5. **Full ownership** — from idea → architecture → code → deploy → monitor
|
||||
6. **Communication** — small team, you explain your own decisions
|
||||
|
||||
### ⚠️ Red/Yellow Flags to Be Aware Of
|
||||
- "Multi-billion dollar vision" is ambitious language — be prepared for startup chaos
|
||||
- "No AI screening" = the founder (Charlie) reads every app personally → **personalize everything**
|
||||
- Early hire = wear many hats, likely no work-life balance initially
|
||||
|
||||
---
|
||||
|
||||
## 📝 APPLICATION FORM — FIELD-BY-FIELD STRATEGY
|
||||
|
||||
The Google Form has **14 fields**. Here's how to make each one count:
|
||||
|
||||
---
|
||||
|
||||
### 1. Full Name *(required)*
|
||||
> Just your name. No tricks here.
|
||||
|
||||
### 2. Email Address *(required)*
|
||||
> Use a professional email. If you have a custom domain, use it — it signals you're technical.
|
||||
|
||||
### 3. LinkedIn / Personal Site / Portfolio *(required)*
|
||||
> **Priority order:** Personal site > LinkedIn > Portfolio
|
||||
> If you have a personal site with projects, that's gold. It shows you ship.
|
||||
> Make sure your LinkedIn headline matches what they want: "Full-Stack Engineer | Next.js + Django | Building AI-powered products"
|
||||
|
||||
### 4. GitHub or Equivalent *(required)*
|
||||
> **Make sure your pinned repos showcase:**
|
||||
> - A full-stack project (React/Next.js + Python backend)
|
||||
> - Something with AI/ML APIs
|
||||
> - Clean READMEs with screenshots, architecture diagrams
|
||||
> - Recent commit activity (shows you're active)
|
||||
|
||||
### 5. Location *(required)*
|
||||
> Be honest. If you're not in London, emphasize timezone overlap willingness.
|
||||
> Example: "Manila, Philippines (happy to work London hours / significant overlap)"
|
||||
|
||||
### 6. Employment Status *(required, radio)*
|
||||
> Options: Employed full-time | Employed part-time | Between roles | Freelancing | Running my own thing
|
||||
> **"Running my own thing" or "Freelancing"** are the strongest signals for this role — it shows self-direction.
|
||||
> "Employed full-time" is fine too — shows you're in demand.
|
||||
|
||||
---
|
||||
|
||||
### 7. 🔥 CRITICAL: "Describe something you built end-to-end" *(required)*
|
||||
|
||||
**This is the MAKE-OR-BREAK question.** They explicitly want: problem → decisions → deployment.
|
||||
|
||||
**Structure your answer like this (aim for 200-350 words):**
|
||||
|
||||
```
|
||||
PROBLEM: [1-2 sentences — what pain point existed]
|
||||
|
||||
WHAT I BUILT: [What the product/feature was, who it served]
|
||||
|
||||
KEY DECISIONS:
|
||||
- Chose [X] over [Y] because [reason] → shows architectural thinking
|
||||
- Used [specific tech] for [specific reason] → shows you don't just follow tutorials
|
||||
- Handled [edge case/challenge] by [solution] → shows production mindset
|
||||
|
||||
RESULT: [Quantifiable if possible — users, performance, revenue, time saved]
|
||||
|
||||
SHIPPED TO: [Where it's live — URL, app store, internal tool]
|
||||
```
|
||||
|
||||
**EXAMPLE (adapt to your experience):**
|
||||
|
||||
> I noticed freelancers in my network were losing 5-10 hours/week manually creating client proposals. I built ProposalPilot — an AI-powered proposal generator.
|
||||
>
|
||||
> Frontend: Next.js with TailwindCSS, deployed on Vercel. Backend: Django REST API on AWS ECS with PostgreSQL. The AI pipeline used OpenAI's API for content generation and a custom prompt chaining system I built to maintain brand voice consistency across sections.
|
||||
>
|
||||
> Key decisions: I chose Django over Express because I needed robust ORM support for complex relational data (clients, templates, proposal versions). I containerized each service with Docker and used GitHub Actions for CI/CD. For the AI layer, I implemented streaming responses so users see content generating in real-time rather than waiting 15-20 seconds for a full response.
|
||||
>
|
||||
> The hardest part was handling rate limits and failures from OpenAI gracefully — I built a retry queue with exponential backoff and a fallback template system so proposals never fail completely.
|
||||
>
|
||||
> Result: 40+ active users, avg. proposal creation time dropped from 3 hours to 20 minutes. The project is live at [URL].
|
||||
|
||||
---
|
||||
|
||||
### 8. Link to Something You've Built *(optional but DO IT)*
|
||||
> This is your proof. Link to:
|
||||
> - A live product URL (best)
|
||||
> - A GitHub repo with a stellar README + demo GIF
|
||||
> - A Loom walkthrough of your project
|
||||
> - A technical blog post about the build
|
||||
|
||||
### 9. 🔥 AI/ML API Experience *(optional but CRITICAL for this role)*
|
||||
|
||||
**They specifically mention: OpenAI, ElevenLabs, Replicate, Whisper, Stable Diffusion**
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
WHAT I BUILT: [Specific project using AI APIs]
|
||||
APIS USED: [List them — the more the better]
|
||||
WHAT I LEARNED: [Focus on production challenges, not just "I called the API"]
|
||||
```
|
||||
|
||||
**EXAMPLE:**
|
||||
|
||||
> I built an AI voice-over tool for content creators using ElevenLabs for TTS, OpenAI for script optimization, and Whisper for transcription/captioning. The pipeline: user uploads a script → GPT-4 optimizes it for spoken delivery → ElevenLabs generates audio with voice cloning → Whisper generates timestamped subtitles.
|
||||
>
|
||||
> Key learnings: ElevenLabs' streaming API is great for previews but you need the non-streaming endpoint for production-quality audio. I learned to manage API costs by implementing a caching layer — identical scripts don't regenerate audio. Also built a webhook system since audio generation is async and can take 10-30 seconds for long content.
|
||||
>
|
||||
> The biggest insight was that prompt engineering for TTS scripts is fundamentally different from chat — you need to engineer for prosody, pacing, and emphasis, not just content accuracy.
|
||||
|
||||
---
|
||||
|
||||
### 10. Tech Skills Grid *(required)*
|
||||
|
||||
Rate honestly — they'll verify in interviews. Here's the scale:
|
||||
| Tech | never used | used once | decent | strong | production-level |
|
||||
|------|-----------|-----------|--------|--------|-----------------|
|
||||
| React / Next.js | | | | ← aim here | ← or here |
|
||||
| Python / Django | | | | ← aim here | ← or here |
|
||||
| PostgreSQL | | | | ← aim here | ← or here |
|
||||
| AWS | | | ← minimum | ← ideal | |
|
||||
| REST API design | | | | | ← aim here |
|
||||
| OAuth | | | ← minimum | ← ideal | |
|
||||
| CI/CD | | | ← minimum | ← ideal | |
|
||||
| Docker | | | ← minimum | ← ideal | |
|
||||
|
||||
**Don't lie.** "Decent experience" with honesty beats "production-level" that crumbles in an interview.
|
||||
|
||||
---
|
||||
|
||||
### 11. 🔥 "Why does this role interest you?" *(required)*
|
||||
|
||||
**DO NOT write generic "I love startups" garbage.** They read every application personally.
|
||||
|
||||
**Formula: Mirror their language + show you understand the stage + add a personal hook**
|
||||
|
||||
**EXAMPLE:**
|
||||
|
||||
> Three things stood out:
|
||||
>
|
||||
> First, the ownership. I've worked in teams where I owned a component, not a problem. You're describing the opposite — pick up a problem space, scope it, build it, ship it. That's exactly how I work best. My best projects happened when nobody told me what to build.
|
||||
>
|
||||
> Second, the timing. Being an early engineering hire at a company with existing revenue and PMF is the sweet spot. You've de-risked the "will anyone pay for this?" question, and now it's about building fast enough to capture the market. That's where I thrive.
|
||||
>
|
||||
> Third, the stack and the AI angle. I've been building with Next.js and Django professionally, and I've been deep in the AI API ecosystem for the past year. The idea of owning AI-powered features end-to-end at a company that's actually shipping (not just experimenting) is exactly where I want to be.
|
||||
>
|
||||
> I watched the Loom — Charlie's energy and clarity about the vision is compelling. I want to be part of building this.
|
||||
|
||||
**(Note: mentioning the Loom video by name shows you actually watched it — huge signal)**
|
||||
|
||||
---
|
||||
|
||||
### 12. Salary Expectation *(required)*
|
||||
> Research tips:
|
||||
> - Remote full-stack roles in London-adjacent timezone: £50k-£80k+ for early hires
|
||||
> - If you're outside UK, adjust for cost-of-living but don't lowball yourself
|
||||
> - Frame it: "$XX,000 USD / year — open to discussion based on equity/benefits package"
|
||||
> - Showing flexibility on comp structure (salary + equity) signals founder-mindset
|
||||
|
||||
### 13. How Soon Could You Start? *(required)*
|
||||
> **"Immediately" or "< 2 weeks"** are strongest signals for an early-stage startup that needs to move fast.
|
||||
> If you need to give notice, "< 1 month" is still fine.
|
||||
|
||||
### 14. Loom Video *(optional — but THIS is your secret weapon)*
|
||||
|
||||
**This is how you separate yourself from 95% of applicants.**
|
||||
|
||||
**Record a 2-minute Loom with this structure:**
|
||||
- **0:00-0:15** — "Hi Charlie, I'm [name], [one-line positioning]"
|
||||
- **0:15-0:45** — Quick walkthrough of something you built (screen share a project)
|
||||
- **0:45-1:30** — Why THIS role specifically (mirror their language: ownership, velocity, AI)
|
||||
- **1:30-2:00** — "Here's what I'd build first if I joined" (show you've thought about their product)
|
||||
|
||||
**Tips:**
|
||||
- Use their founder's name (Charlie — from the Loom video)
|
||||
- Show energy and enthusiasm — match their "going to the moon" vibe
|
||||
- Share your screen showing a real project, not just a talking head
|
||||
- Keep it under 2 minutes — respect their time
|
||||
|
||||
---
|
||||
|
||||
## 🏆 APPLICATION CHECKLIST
|
||||
|
||||
Before you submit, verify:
|
||||
|
||||
- [ ] GitHub pinned repos are updated with best projects + clean READMEs
|
||||
- [ ] LinkedIn headline/summary reflects full-stack + AI capabilities
|
||||
- [ ] "Built end-to-end" answer follows Problem → Decisions → Result structure
|
||||
- [ ] AI/ML answer shows PRODUCTION challenges, not just tutorial-level usage
|
||||
- [ ] "Why this role" mentions specifics from THEIR posting (Loom, microservices, PMF)
|
||||
- [ ] Salary research is done — give a confident range
|
||||
- [ ] Loom video recorded (2 min, high energy, shows a real project)
|
||||
- [ ] All required fields filled (13 required, 1 optional)
|
||||
- [ ] Re-read everything — no typos, no generic language
|
||||
|
||||
---
|
||||
|
||||
## 💡 POWER MOVES (Stand Out Tactics)
|
||||
|
||||
1. **Build a mini demo** — Before applying, spend 2-4 hours building a tiny microservice that solves a problem relevant to their space (e.g., an AI-powered lead qualifier). Link it in your "built something" answer. Nothing says "I ship" like shipping something FOR them.
|
||||
|
||||
2. **Reference the Loom** — The founder recorded a 7-minute Loom. Most applicants won't watch it. Reference specific things from it to prove you did.
|
||||
|
||||
3. **Show, don't tell** — Every claim should have a link, a repo, or a demo. "I've built with AI APIs" < "Here's the repo where I integrated OpenAI + ElevenLabs: [link]"
|
||||
|
||||
4. **Think like a founder** — In your "why this role" answer, mention what you'd want to build first. Shows you're already thinking about their product, not just your career.
|
||||
|
||||
5. **Follow up** — If you can find Charlie on LinkedIn/Twitter, send a short "Just applied — excited about [specific thing]" message 24h after applying.
|
||||
BIN
job-page-top.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
1
justvitamin-build
Submodule
BIN
jv-before-pdp.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
1
jv_api_data.json
Normal file
198
lib/agent-worker.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { toolDefs, executeTool } from "./tools.js";
|
||||
import * as store from "./store.js";
|
||||
import * as tg from "./telegram.js";
|
||||
|
||||
const client = new Anthropic();
|
||||
const MODEL = "claude-sonnet-4-20250514";
|
||||
const MAX_TURNS = 50;
|
||||
|
||||
// Map of agentId -> resolve function for when user replies
|
||||
const waitingForUser = new Map<number, (response: string) => void>();
|
||||
|
||||
export function isWaitingForUser(agentId: number): boolean {
|
||||
return waitingForUser.has(agentId);
|
||||
}
|
||||
|
||||
export function resolveUserResponse(agentId: number, response: string): void {
|
||||
const resolve = waitingForUser.get(agentId);
|
||||
if (resolve) {
|
||||
waitingForUser.delete(agentId);
|
||||
resolve(response);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSystemPrompt(task: string): string {
|
||||
return `You are an autonomous coding agent. You have been assigned a specific task.
|
||||
|
||||
YOUR TASK:
|
||||
${task}
|
||||
|
||||
GUIDELINES:
|
||||
- Work independently to complete the task
|
||||
- Use the bash tool for running commands, git, etc.
|
||||
- Use read_file, write_file, edit_file for file operations
|
||||
- When you're done, call the "done" tool with a summary
|
||||
- If you need user input or a decision, call "ask_user"
|
||||
- Be efficient — don't explain what you're about to do, just do it
|
||||
- If something fails, try to fix it yourself before asking the user
|
||||
|
||||
WORKING DIRECTORY: ${process.cwd()}`;
|
||||
}
|
||||
|
||||
export async function runAgent(agentId: number): Promise<void> {
|
||||
const agent = store.getAgent(agentId);
|
||||
if (!agent) return;
|
||||
|
||||
store.updateAgent(agentId, { status: "working" });
|
||||
store.addLog(agentId, "system", `Agent started: ${agent.task}`);
|
||||
|
||||
const messages: Anthropic.MessageParam[] = [
|
||||
{ role: "user", content: agent.task },
|
||||
];
|
||||
|
||||
let turns = 0;
|
||||
|
||||
try {
|
||||
while (turns < MAX_TURNS) {
|
||||
turns++;
|
||||
|
||||
const response = await client.messages.create({
|
||||
model: MODEL,
|
||||
max_tokens: 8096,
|
||||
system: buildSystemPrompt(agent.task),
|
||||
tools: toolDefs as any,
|
||||
messages,
|
||||
});
|
||||
|
||||
// Collect assistant content
|
||||
const assistantContent = response.content;
|
||||
messages.push({ role: "assistant", content: assistantContent });
|
||||
|
||||
// Log text blocks (no Telegram notification — reduces noise)
|
||||
for (const block of assistantContent) {
|
||||
if (block.type === "text" && block.text.trim()) {
|
||||
store.addLog(agentId, "assistant", block.text);
|
||||
}
|
||||
}
|
||||
|
||||
// If no tool use, we're done
|
||||
if (response.stop_reason !== "tool_use") {
|
||||
store.updateAgent(agentId, {
|
||||
status: "done",
|
||||
summary: "Completed (no more actions)",
|
||||
});
|
||||
store.addLog(agentId, "system", "Agent finished (end_turn)");
|
||||
await tg.send(
|
||||
`✅ *Agent #${agentId}* finished.\nTask: ${agent.task}`,
|
||||
agent.chat_id,
|
||||
{ reply_to: agent.thread_msg_id || undefined }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process tool calls
|
||||
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
||||
|
||||
for (const block of assistantContent) {
|
||||
if (block.type !== "tool_use") continue;
|
||||
|
||||
const toolName = block.name;
|
||||
const toolInput = block.input as Record<string, unknown>;
|
||||
|
||||
store.addLog(
|
||||
agentId,
|
||||
"tool",
|
||||
`${toolName}: ${JSON.stringify(toolInput).slice(0, 500)}`
|
||||
);
|
||||
|
||||
if (toolName === "ask_user") {
|
||||
// Pause and wait for user response
|
||||
store.updateAgent(agentId, { status: "waiting" });
|
||||
await tg.send(
|
||||
`❓ *Agent #${agentId}* needs your input:\n\n${toolInput.question}`,
|
||||
agent.chat_id,
|
||||
{
|
||||
reply_to: agent.thread_msg_id || undefined,
|
||||
keyboard: [
|
||||
[{ text: "💬 Reply", callback_data: `talk_${agentId}` }],
|
||||
],
|
||||
}
|
||||
);
|
||||
store.addLog(agentId, "system", `Waiting for user: ${toolInput.question}`);
|
||||
|
||||
// Wait for user response
|
||||
const userResponse = await new Promise<string>((resolve) => {
|
||||
waitingForUser.set(agentId, resolve);
|
||||
});
|
||||
|
||||
store.updateAgent(agentId, { status: "working" });
|
||||
store.addLog(agentId, "user", `User replied: ${userResponse}`);
|
||||
|
||||
toolResults.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: block.id,
|
||||
content: `User responded: ${userResponse}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolName === "done") {
|
||||
const summary = (toolInput.summary as string) || "Task completed";
|
||||
store.updateAgent(agentId, { status: "done", summary });
|
||||
store.addLog(agentId, "system", `Done: ${summary}`);
|
||||
await tg.send(
|
||||
`✅ *Agent #${agentId}* completed!\n\n*Summary:* ${summary}\n*Task:* ${agent.task}`,
|
||||
agent.chat_id,
|
||||
{ reply_to: agent.thread_msg_id || undefined }
|
||||
);
|
||||
|
||||
toolResults.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: block.id,
|
||||
content: summary,
|
||||
});
|
||||
// Push tool results and stop
|
||||
messages.push({ role: "user", content: toolResults });
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
const { result } = executeTool(toolName, toolInput);
|
||||
store.addLog(
|
||||
agentId,
|
||||
"tool_result",
|
||||
`${toolName} → ${result.slice(0, 500)}`
|
||||
);
|
||||
|
||||
toolResults.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: block.id,
|
||||
content: result,
|
||||
});
|
||||
}
|
||||
|
||||
messages.push({ role: "user", content: toolResults });
|
||||
}
|
||||
|
||||
// Hit max turns
|
||||
store.updateAgent(agentId, {
|
||||
status: "done",
|
||||
summary: `Stopped after ${MAX_TURNS} turns`,
|
||||
});
|
||||
await tg.send(
|
||||
`⚠️ *Agent #${agentId}* hit max turns (${MAX_TURNS}). Task: ${agent.task}`,
|
||||
agent.chat_id,
|
||||
{ reply_to: agent.thread_msg_id || undefined }
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.error(`[agent ${agentId}] error:`, e);
|
||||
store.updateAgent(agentId, { status: "error", error: e.message });
|
||||
store.addLog(agentId, "error", e.message);
|
||||
await tg.send(
|
||||
`❌ *Agent #${agentId}* error:\n${e.message?.slice(0, 500)}`,
|
||||
agent.chat_id,
|
||||
{ reply_to: agent.thread_msg_id || undefined }
|
||||
);
|
||||
}
|
||||
}
|
||||
341
lib/bot.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import * as tg from "./telegram.js";
|
||||
import * as store from "./store.js";
|
||||
import { runAgent, isWaitingForUser, resolveUserResponse } from "./agent-worker.js";
|
||||
|
||||
// Track which agents are running (in-process)
|
||||
const runningAgents = new Set<number>();
|
||||
|
||||
// Track "talk mode" — chatId -> agentId they're talking to
|
||||
const talkMode = new Map<number, number>();
|
||||
|
||||
const STATUS_EMOJI: Record<string, string> = {
|
||||
spawning: "🔄",
|
||||
working: "⚡",
|
||||
waiting: "❓",
|
||||
done: "✅",
|
||||
error: "❌",
|
||||
killed: "🛑",
|
||||
};
|
||||
|
||||
async function handleMessage(msg: NonNullable<tg.TelegramUpdate["message"]>) {
|
||||
const chatId = msg.chat.id;
|
||||
const text = (msg.text || "").trim();
|
||||
|
||||
if (!tg.isAllowed(chatId)) return;
|
||||
|
||||
// Check if user is in talk mode with an agent
|
||||
if (talkMode.has(chatId) && !text.startsWith("/")) {
|
||||
const agentId = talkMode.get(chatId)!;
|
||||
talkMode.delete(chatId);
|
||||
|
||||
if (isWaitingForUser(agentId)) {
|
||||
resolveUserResponse(agentId, text);
|
||||
await tg.send(`💬 Sent to Agent #${agentId}`, chatId);
|
||||
} else {
|
||||
// Agent is working but user wants to interject — add as follow-up
|
||||
// For now just queue it
|
||||
store.addLog(agentId, "user", text);
|
||||
await tg.send(
|
||||
`📝 Noted for Agent #${agentId}. It's currently working — your message will be seen when it next checks.`,
|
||||
chatId
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a reply to an agent thread
|
||||
if (msg.reply_to_message && !text.startsWith("/")) {
|
||||
const replyToId = msg.reply_to_message.message_id;
|
||||
const agent = store.findAgentByThreadMsg(replyToId);
|
||||
if (agent && isWaitingForUser(agent.id)) {
|
||||
resolveUserResponse(agent.id, text);
|
||||
await tg.send(`💬 Sent to Agent #${agent.id}`, chatId);
|
||||
return;
|
||||
}
|
||||
// Also check all agents for this chat — find closest thread
|
||||
const agents = store.listAgents(String(chatId));
|
||||
for (const a of agents) {
|
||||
if (a.thread_msg_id === replyToId && isWaitingForUser(a.id)) {
|
||||
resolveUserResponse(a.id, text);
|
||||
await tg.send(`💬 Sent to Agent #${a.id}`, chatId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commands
|
||||
if (text.startsWith("/new ") || text.startsWith("/new@")) {
|
||||
const task = text.replace(/^\/new(@\w+)?\s*/, "").trim();
|
||||
if (!task) {
|
||||
await tg.send("Usage: `/new <task description>`", chatId);
|
||||
return;
|
||||
}
|
||||
await spawnAgent(chatId, task);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text === "/board" || text.startsWith("/board@")) {
|
||||
await showBoard(chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.startsWith("/kill ") || text.startsWith("/kill@")) {
|
||||
const idStr = text.replace(/^\/kill(@\w+)?\s*/, "").trim();
|
||||
const id = parseInt(idStr);
|
||||
if (isNaN(id)) {
|
||||
await tg.send("Usage: `/kill <agent_id>`", chatId);
|
||||
return;
|
||||
}
|
||||
await killAgent(chatId, id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.startsWith("/logs ") || text.startsWith("/logs@")) {
|
||||
const idStr = text.replace(/^\/logs(@\w+)?\s*/, "").trim();
|
||||
const id = parseInt(idStr);
|
||||
if (isNaN(id)) {
|
||||
await tg.send("Usage: `/logs <agent_id>`", chatId);
|
||||
return;
|
||||
}
|
||||
await showLogs(chatId, id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.startsWith("/talk ") || text.startsWith("/talk@")) {
|
||||
const idStr = text.replace(/^\/talk(@\w+)?\s*/, "").trim();
|
||||
const id = parseInt(idStr);
|
||||
if (isNaN(id)) {
|
||||
await tg.send("Usage: `/talk <agent_id>`", chatId);
|
||||
return;
|
||||
}
|
||||
talkMode.set(chatId, id);
|
||||
await tg.send(
|
||||
`💬 You're now talking to *Agent #${id}*. Send your message (or /cancel):`,
|
||||
chatId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text === "/cancel" || text.startsWith("/cancel@")) {
|
||||
if (talkMode.has(chatId)) {
|
||||
talkMode.delete(chatId);
|
||||
await tg.send("Cancelled talk mode.", chatId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (text === "/clear" || text.startsWith("/clear@")) {
|
||||
await clearChat(chatId, msg.message_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text === "/help" || text === "/start" || text.startsWith("/help@") || text.startsWith("/start@")) {
|
||||
await tg.send(
|
||||
`🤖 *Agent Orchestrator*
|
||||
|
||||
Commands:
|
||||
/new <task> — Spawn a new agent
|
||||
/board — View all agents
|
||||
/logs <id> — Agent activity log
|
||||
/talk <id> — Send message to agent
|
||||
/kill <id> — Stop an agent
|
||||
/clear — Clear chat messages
|
||||
/help — This message
|
||||
|
||||
Or *reply* to any agent message to talk to it directly.`,
|
||||
chatId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown
|
||||
if (text.startsWith("/")) {
|
||||
await tg.send("Unknown command. Try /help", chatId);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCallback(cb: NonNullable<tg.TelegramUpdate["callback_query"]>) {
|
||||
const chatId = cb.message?.chat?.id;
|
||||
if (!chatId || !tg.isAllowed(chatId)) return;
|
||||
|
||||
const data = cb.data || "";
|
||||
await tg.answerCallback(cb.id);
|
||||
|
||||
if (data.startsWith("logs_")) {
|
||||
const id = parseInt(data.replace("logs_", ""));
|
||||
await showLogs(chatId, id);
|
||||
} else if (data.startsWith("talk_")) {
|
||||
const id = parseInt(data.replace("talk_", ""));
|
||||
talkMode.set(chatId, id);
|
||||
await tg.send(
|
||||
`💬 Talking to *Agent #${id}*. Send your message:`,
|
||||
chatId
|
||||
);
|
||||
} else if (data.startsWith("kill_")) {
|
||||
const id = parseInt(data.replace("kill_", ""));
|
||||
await killAgent(chatId, id);
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnAgent(chatId: number, task: string) {
|
||||
const agent = store.createAgent(task, String(chatId));
|
||||
|
||||
// Send initial message and save its ID for threading
|
||||
const res = await tg.send(
|
||||
`🤖 *Agent #${agent.id}* spawned\n*Task:* ${task}\n*Status:* spawning...`,
|
||||
chatId,
|
||||
{ keyboard: tg.agentKeyboard(agent.id) }
|
||||
);
|
||||
|
||||
if (res?.result?.message_id) {
|
||||
store.updateAgent(agent.id, { thread_msg_id: res.result.message_id });
|
||||
}
|
||||
|
||||
// Run agent in background (non-blocking)
|
||||
runningAgents.add(agent.id);
|
||||
runAgent(agent.id)
|
||||
.catch((e) => console.error(`[agent ${agent.id}] fatal:`, e))
|
||||
.finally(() => runningAgents.delete(agent.id));
|
||||
}
|
||||
|
||||
async function showBoard(chatId: number) {
|
||||
const agents = store.listAgents(String(chatId));
|
||||
|
||||
if (agents.length === 0) {
|
||||
await tg.send("No agents yet. Use `/new <task>` to spawn one.", chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
let board = "📋 *Agent Board*\n\n";
|
||||
board += "```\n";
|
||||
board += " # | Status | Task\n";
|
||||
board += "----|--------|---------------------------\n";
|
||||
|
||||
for (const a of agents.slice(0, 20)) {
|
||||
const emoji = STATUS_EMOJI[a.status] || "❔";
|
||||
const taskShort = a.task.length > 30 ? a.task.slice(0, 27) + "..." : a.task;
|
||||
board += ` ${String(a.id).padStart(2)} | ${emoji} ${a.status.padEnd(4).slice(0, 4)} | ${taskShort}\n`;
|
||||
}
|
||||
board += "```\n";
|
||||
|
||||
// Show summaries for done agents
|
||||
const done = agents.filter((a) => a.summary);
|
||||
if (done.length > 0) {
|
||||
board += "\n*Completed:*\n";
|
||||
for (const a of done.slice(0, 5)) {
|
||||
board += `• #${a.id}: ${a.summary}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
await tg.send(board, chatId);
|
||||
}
|
||||
|
||||
async function killAgent(chatId: number, agentId: number) {
|
||||
const agent = store.getAgent(agentId);
|
||||
if (!agent) {
|
||||
await tg.send(`Agent #${agentId} not found.`, chatId);
|
||||
return;
|
||||
}
|
||||
if (agent.chat_id !== String(chatId)) {
|
||||
await tg.send(`Agent #${agentId} doesn't belong to you.`, chatId);
|
||||
return;
|
||||
}
|
||||
if (agent.status === "done" || agent.status === "killed") {
|
||||
await tg.send(`Agent #${agentId} is already ${agent.status}.`, chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
store.updateAgent(agentId, { status: "killed", summary: "Killed by user" });
|
||||
store.addLog(agentId, "system", "Killed by user");
|
||||
|
||||
// If waiting for user, resolve with cancellation
|
||||
if (isWaitingForUser(agentId)) {
|
||||
resolveUserResponse(agentId, "[USER CANCELLED THIS AGENT]");
|
||||
}
|
||||
|
||||
await tg.send(`🛑 Agent #${agentId} killed.`, chatId);
|
||||
}
|
||||
|
||||
async function showLogs(chatId: number, agentId: number) {
|
||||
const agent = store.getAgent(agentId);
|
||||
if (!agent) {
|
||||
await tg.send(`Agent #${agentId} not found.`, chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = store.getLogs(agentId, 15);
|
||||
if (logs.length === 0) {
|
||||
await tg.send(`No logs for Agent #${agentId}.`, chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
let text = `📋 *Logs — Agent #${agentId}*\n_${agent.task}_\n\n`;
|
||||
for (const log of logs.reverse()) {
|
||||
const role = log.role.toUpperCase().padEnd(6).slice(0, 6);
|
||||
const content = log.content.length > 150 ? log.content.slice(0, 147) + "..." : log.content;
|
||||
text += `\`${role}\` ${content}\n\n`;
|
||||
}
|
||||
|
||||
await tg.send(text, chatId, { keyboard: tg.agentKeyboard(agentId) });
|
||||
}
|
||||
|
||||
async function clearChat(chatId: number, commandMsgId: number) {
|
||||
// Delete the /clear command message itself first
|
||||
await tg.deleteMessage(chatId, commandMsgId);
|
||||
|
||||
// Telegram only allows deleting messages less than 48h old.
|
||||
// We walk backwards from the command message ID, trying to delete recent messages.
|
||||
const statusMsg = await tg.send("🧹 Clearing chat...", chatId);
|
||||
const statusMsgId: number | undefined = statusMsg?.result?.message_id;
|
||||
|
||||
let deleted = 0;
|
||||
let misses = 0;
|
||||
const MAX_MISSES = 10; // stop after 10 consecutive failures (hit old messages or gap)
|
||||
|
||||
// Walk backwards from the /clear message
|
||||
for (let id = commandMsgId - 1; id > 0 && misses < MAX_MISSES; id--) {
|
||||
const ok = await tg.deleteMessage(chatId, id);
|
||||
if (ok) {
|
||||
deleted++;
|
||||
misses = 0;
|
||||
} else {
|
||||
misses++;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the status message too, then send a clean confirmation
|
||||
if (statusMsgId) await tg.deleteMessage(chatId, statusMsgId);
|
||||
await tg.send(`🧹 Cleared ${deleted} messages.`, chatId);
|
||||
}
|
||||
|
||||
// --- Main loop ---
|
||||
|
||||
async function main() {
|
||||
console.log("🤖 Telegram Agent Orchestrator starting...");
|
||||
console.log(` Polling for updates...`);
|
||||
|
||||
await tg.send("🤖 Agent Orchestrator is online! Send /help to get started.");
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const updates = await tg.poll();
|
||||
for (const update of updates) {
|
||||
if (update.message) {
|
||||
handleMessage(update.message).catch((e) =>
|
||||
console.error("[handle msg]", e)
|
||||
);
|
||||
}
|
||||
if (update.callback_query) {
|
||||
handleCallback(update.callback_query).catch((e) =>
|
||||
console.error("[handle cb]", e)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[main loop]", e);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
179
lib/store.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { mkdirSync } from "fs";
|
||||
|
||||
mkdirSync("data", { recursive: true });
|
||||
const db = new Database("data/agents.db");
|
||||
|
||||
db.run("PRAGMA journal_mode = WAL");
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'spawning',
|
||||
chat_id TEXT NOT NULL,
|
||||
thread_msg_id INTEGER,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
summary TEXT,
|
||||
error TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS agent_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS agent_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
tool_use TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
||||
)
|
||||
`);
|
||||
|
||||
export interface Agent {
|
||||
id: number;
|
||||
task: string;
|
||||
status: string;
|
||||
chat_id: string;
|
||||
thread_msg_id: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
summary: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface AgentLog {
|
||||
id: number;
|
||||
agent_id: number;
|
||||
role: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// --- Agent CRUD ---
|
||||
|
||||
export function createAgent(task: string, chatId: string): Agent {
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO agents (task, status, chat_id) VALUES (?, 'spawning', ?)`
|
||||
);
|
||||
stmt.run(task, chatId);
|
||||
const id = Number(db.query("SELECT last_insert_rowid() as id").get()!.id);
|
||||
return getAgent(id)!;
|
||||
}
|
||||
|
||||
export function getAgent(id: number): Agent | undefined {
|
||||
return db.query(`SELECT * FROM agents WHERE id = ?`).get(id) as
|
||||
| Agent
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export function updateAgent(
|
||||
id: number,
|
||||
updates: Partial<Pick<Agent, "status" | "thread_msg_id" | "summary" | "error">>
|
||||
): void {
|
||||
const fields: string[] = ["updated_at = datetime('now')"];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (updates.status !== undefined) {
|
||||
fields.push("status = ?");
|
||||
values.push(updates.status);
|
||||
}
|
||||
if (updates.thread_msg_id !== undefined) {
|
||||
fields.push("thread_msg_id = ?");
|
||||
values.push(updates.thread_msg_id);
|
||||
}
|
||||
if (updates.summary !== undefined) {
|
||||
fields.push("summary = ?");
|
||||
values.push(updates.summary);
|
||||
}
|
||||
if (updates.error !== undefined) {
|
||||
fields.push("error = ?");
|
||||
values.push(updates.error);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
db.prepare(`UPDATE agents SET ${fields.join(", ")} WHERE id = ?`).run(
|
||||
...values
|
||||
);
|
||||
}
|
||||
|
||||
export function listAgents(chatId?: string): Agent[] {
|
||||
if (chatId) {
|
||||
return db
|
||||
.query(`SELECT * FROM agents WHERE chat_id = ? ORDER BY id DESC`)
|
||||
.all(chatId) as Agent[];
|
||||
}
|
||||
return db.query(`SELECT * FROM agents ORDER BY id DESC`).all() as Agent[];
|
||||
}
|
||||
|
||||
export function getActiveAgents(): Agent[] {
|
||||
return db
|
||||
.query(
|
||||
`SELECT * FROM agents WHERE status IN ('spawning', 'working', 'waiting') ORDER BY id`
|
||||
)
|
||||
.all() as Agent[];
|
||||
}
|
||||
|
||||
export function findAgentByThreadMsg(messageId: number): Agent | undefined {
|
||||
return db
|
||||
.query(`SELECT * FROM agents WHERE thread_msg_id = ?`)
|
||||
.get(messageId) as Agent | undefined;
|
||||
}
|
||||
|
||||
// --- Logs ---
|
||||
|
||||
export function addLog(agentId: number, role: string, content: string): void {
|
||||
db.prepare(
|
||||
`INSERT INTO agent_logs (agent_id, role, content) VALUES (?, ?, ?)`
|
||||
).run(agentId, role, content);
|
||||
}
|
||||
|
||||
export function getLogs(agentId: number, limit = 20): AgentLog[] {
|
||||
return db
|
||||
.query(
|
||||
`SELECT * FROM agent_logs WHERE agent_id = ? ORDER BY id DESC LIMIT ?`
|
||||
)
|
||||
.all(agentId, limit) as AgentLog[];
|
||||
}
|
||||
|
||||
// --- Messages (conversation history) ---
|
||||
|
||||
export function addMessage(
|
||||
agentId: number,
|
||||
role: string,
|
||||
content: string,
|
||||
toolUse?: string
|
||||
): void {
|
||||
db.prepare(
|
||||
`INSERT INTO agent_messages (agent_id, role, content, tool_use) VALUES (?, ?, ?, ?)`
|
||||
).run(agentId, role, content, toolUse || null);
|
||||
}
|
||||
|
||||
export function getMessages(
|
||||
agentId: number
|
||||
): Array<{ role: string; content: string; tool_use: string | null }> {
|
||||
return db
|
||||
.query(
|
||||
`SELECT role, content, tool_use FROM agent_messages WHERE agent_id = ? ORDER BY id ASC`
|
||||
)
|
||||
.all(agentId) as Array<{
|
||||
role: string;
|
||||
content: string;
|
||||
tool_use: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default db;
|
||||
142
lib/telegram.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
|
||||
const DEFAULT_CHAT_ID = process.env.TELEGRAM_CHAT_ID!;
|
||||
const ALLOWED_IDS = new Set(
|
||||
(process.env.TELEGRAM_ALLOWED_CHAT_IDS || DEFAULT_CHAT_ID)
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
);
|
||||
|
||||
const api = async (method: string, body?: Record<string, unknown>) => {
|
||||
const res = await fetch(
|
||||
`https://api.telegram.org/bot${BOT_TOKEN}/${method}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}
|
||||
);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export function isAllowed(chatId: number | string): boolean {
|
||||
return ALLOWED_IDS.has(String(chatId));
|
||||
}
|
||||
|
||||
export async function send(
|
||||
text: string,
|
||||
chatId: string | number = DEFAULT_CHAT_ID,
|
||||
opts?: { reply_to?: number; keyboard?: InlineKeyboard }
|
||||
): Promise<any> {
|
||||
if (!isAllowed(chatId)) return null;
|
||||
|
||||
// Telegram limits messages to 4096 chars
|
||||
const truncated =
|
||||
text.length > 4000 ? text.slice(0, 4000) + "\n\n... (truncated)" : text;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
chat_id: chatId,
|
||||
text: truncated,
|
||||
parse_mode: "Markdown",
|
||||
};
|
||||
if (opts?.reply_to) body.reply_to_message_id = opts.reply_to;
|
||||
if (opts?.keyboard) {
|
||||
body.reply_markup = { inline_keyboard: opts.keyboard };
|
||||
}
|
||||
return api("sendMessage", body);
|
||||
}
|
||||
|
||||
export async function editMessage(
|
||||
chatId: string | number,
|
||||
messageId: number,
|
||||
text: string,
|
||||
keyboard?: InlineKeyboard
|
||||
): Promise<any> {
|
||||
const truncated =
|
||||
text.length > 4000 ? text.slice(0, 4000) + "\n\n... (truncated)" : text;
|
||||
const body: Record<string, unknown> = {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: truncated,
|
||||
parse_mode: "Markdown",
|
||||
};
|
||||
if (keyboard) body.reply_markup = { inline_keyboard: keyboard };
|
||||
return api("editMessageText", body);
|
||||
}
|
||||
|
||||
export async function answerCallback(
|
||||
callbackId: string,
|
||||
text?: string
|
||||
): Promise<any> {
|
||||
return api("answerCallbackQuery", {
|
||||
callback_query_id: callbackId,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
export type InlineKeyboard = Array<
|
||||
Array<{ text: string; callback_data: string }>
|
||||
>;
|
||||
|
||||
export function agentKeyboard(agentId: number): InlineKeyboard {
|
||||
return [
|
||||
[
|
||||
{ text: "📋 Logs", callback_data: `logs_${agentId}` },
|
||||
{ text: "💬 Talk", callback_data: `talk_${agentId}` },
|
||||
{ text: "🛑 Kill", callback_data: `kill_${agentId}` },
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
|
||||
export interface TelegramUpdate {
|
||||
update_id: number;
|
||||
message?: {
|
||||
message_id: number;
|
||||
from?: { id: number; first_name: string };
|
||||
chat: { id: number; type: string };
|
||||
text?: string;
|
||||
reply_to_message?: { message_id: number };
|
||||
};
|
||||
callback_query?: {
|
||||
id: string;
|
||||
from: { id: number };
|
||||
message?: { message_id: number; chat: { id: number } };
|
||||
data?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function poll(): Promise<TelegramUpdate[]> {
|
||||
try {
|
||||
const data = await api("getUpdates", {
|
||||
offset,
|
||||
timeout: 30,
|
||||
allowed_updates: ["message", "callback_query"],
|
||||
});
|
||||
const updates: TelegramUpdate[] = data.result || [];
|
||||
if (updates.length > 0) {
|
||||
offset = updates[updates.length - 1].update_id + 1;
|
||||
}
|
||||
return updates;
|
||||
} catch (e) {
|
||||
console.error("[telegram] poll error:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMessage(
|
||||
chatId: string | number,
|
||||
messageId: number
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const res = await api("deleteMessage", {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
});
|
||||
return !!res?.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default { send, editMessage, deleteMessage, poll, isAllowed, agentKeyboard, answerCallback };
|
||||
194
lib/tools.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { execSync } from "child_process";
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const toolDefs: ToolDefinition[] = [
|
||||
{
|
||||
name: "bash",
|
||||
description:
|
||||
"Execute a bash command. Returns stdout and stderr. Use for running commands, installing packages, git, etc. Timeout: 120s.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: {
|
||||
type: "string",
|
||||
description: "The bash command to execute",
|
||||
},
|
||||
},
|
||||
required: ["command"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read_file",
|
||||
description:
|
||||
"Read the contents of a file. Returns the text content. Use for examining code, configs, etc.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to the file" },
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Max lines to read (default: all)",
|
||||
},
|
||||
offset: {
|
||||
type: "number",
|
||||
description: "Line to start from, 1-indexed (default: 1)",
|
||||
},
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "write_file",
|
||||
description:
|
||||
"Write content to a file. Creates parent directories if needed. Overwrites existing files.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to the file" },
|
||||
content: {
|
||||
type: "string",
|
||||
description: "Content to write",
|
||||
},
|
||||
},
|
||||
required: ["path", "content"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edit_file",
|
||||
description:
|
||||
"Edit a file by replacing exact text. oldText must match exactly including whitespace.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to the file" },
|
||||
old_text: {
|
||||
type: "string",
|
||||
description: "Exact text to find",
|
||||
},
|
||||
new_text: {
|
||||
type: "string",
|
||||
description: "Replacement text",
|
||||
},
|
||||
},
|
||||
required: ["path", "old_text", "new_text"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
description:
|
||||
"Call this when the task is fully complete. Provide a short summary of what was accomplished.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
summary: {
|
||||
type: "string",
|
||||
description: "Short summary of what was done",
|
||||
},
|
||||
},
|
||||
required: ["summary"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ask_user",
|
||||
description:
|
||||
"Ask the user a question when you need clarification or a decision. The user will respond via Telegram.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
question: {
|
||||
type: "string",
|
||||
description: "The question to ask",
|
||||
},
|
||||
},
|
||||
required: ["question"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function executeTool(
|
||||
name: string,
|
||||
input: Record<string, unknown>
|
||||
): { result: string; isDone?: boolean; isQuestion?: boolean } {
|
||||
try {
|
||||
switch (name) {
|
||||
case "bash": {
|
||||
const cmd = input.command as string;
|
||||
try {
|
||||
const output = execSync(cmd, {
|
||||
encoding: "utf-8",
|
||||
timeout: 120_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
const trimmed = output.length > 10000
|
||||
? output.slice(0, 10000) + "\n...(truncated)"
|
||||
: output;
|
||||
return { result: trimmed || "(no output)" };
|
||||
} catch (e: any) {
|
||||
const stderr = e.stderr || "";
|
||||
const stdout = e.stdout || "";
|
||||
return {
|
||||
result: `Exit code: ${e.status}\nSTDOUT: ${stdout}\nSTDERR: ${stderr}`.slice(
|
||||
0,
|
||||
5000
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case "read_file": {
|
||||
const path = input.path as string;
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const offset = ((input.offset as number) || 1) - 1;
|
||||
const limit = (input.limit as number) || lines.length;
|
||||
const slice = lines.slice(offset, offset + limit).join("\n");
|
||||
return {
|
||||
result:
|
||||
slice.length > 10000
|
||||
? slice.slice(0, 10000) + "\n...(truncated)"
|
||||
: slice,
|
||||
};
|
||||
}
|
||||
|
||||
case "write_file": {
|
||||
const path = input.path as string;
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, input.content as string, "utf-8");
|
||||
return { result: `Written ${(input.content as string).length} bytes to ${path}` };
|
||||
}
|
||||
|
||||
case "edit_file": {
|
||||
const path = input.path as string;
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const oldText = input.old_text as string;
|
||||
const newText = input.new_text as string;
|
||||
if (!content.includes(oldText)) {
|
||||
return { result: `ERROR: old_text not found in ${path}` };
|
||||
}
|
||||
writeFileSync(path, content.replace(oldText, newText), "utf-8");
|
||||
return { result: `Edited ${path}` };
|
||||
}
|
||||
|
||||
case "done": {
|
||||
return { result: input.summary as string, isDone: true };
|
||||
}
|
||||
|
||||
case "ask_user": {
|
||||
return { result: input.question as string, isQuestion: true };
|
||||
}
|
||||
|
||||
default:
|
||||
return { result: `Unknown tool: ${name}` };
|
||||
}
|
||||
} catch (e: any) {
|
||||
return { result: `Tool error: ${e.message}` };
|
||||
}
|
||||
}
|
||||
82
optimize_images.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Optimize all landing images: resize + compress + strip EXIF.
|
||||
Also set hero-concept-1.jpg as the new hero image."""
|
||||
import sys, os, shutil
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
from PIL import Image
|
||||
|
||||
IMG_DIR = "pledge-now-pay-later/public/images/landing"
|
||||
BRAND_DIR = "pledge-now-pay-later/brand/photography"
|
||||
|
||||
# Max dimensions per image type
|
||||
# Portrait (4:5 or 1:1) → max 1000px long side
|
||||
# Landscape (16:9) → max 1200px wide
|
||||
MAX_LANDSCAPE = 1200
|
||||
MAX_PORTRAIT = 1000
|
||||
QUALITY = 80
|
||||
|
||||
def optimize(path, max_long_side):
|
||||
"""Resize + compress + strip EXIF. Returns (old_size, new_size)."""
|
||||
old_size = os.path.getsize(path)
|
||||
img = Image.open(path)
|
||||
|
||||
# Strip EXIF by creating new image
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
|
||||
# Resize if larger than max
|
||||
w, h = img.size
|
||||
long_side = max(w, h)
|
||||
if long_side > max_long_side:
|
||||
ratio = max_long_side / long_side
|
||||
new_w = int(w * ratio)
|
||||
new_h = int(h * ratio)
|
||||
img = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
|
||||
# Save with progressive JPEG, quality 80, optimized
|
||||
img.save(path, "JPEG", quality=QUALITY, optimize=True, progressive=True)
|
||||
new_size = os.path.getsize(path)
|
||||
return old_size, new_size, img.size
|
||||
|
||||
# Step 1: Set hero-concept-1 as the main hero
|
||||
hero_src = os.path.join(IMG_DIR, "hero-concept-1.jpg")
|
||||
hero_dst = os.path.join(IMG_DIR, "00-hero.jpg")
|
||||
if os.path.exists(hero_src):
|
||||
shutil.copy2(hero_src, hero_dst)
|
||||
# Also copy to brand dir
|
||||
shutil.copy2(hero_src, os.path.join(BRAND_DIR, "00-hero.jpg"))
|
||||
print(f"Set hero: hero-concept-1.jpg -> 00-hero.jpg")
|
||||
|
||||
# Step 2: Clean up concept images
|
||||
for f in ["hero-concept-1.jpg", "hero-concept-2.jpg", "hero-concept-3.jpg"]:
|
||||
p = os.path.join(IMG_DIR, f)
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
print(f"Cleaned: {f}")
|
||||
|
||||
# Step 3: Optimize all images
|
||||
total_old = 0
|
||||
total_new = 0
|
||||
|
||||
for fname in sorted(os.listdir(IMG_DIR)):
|
||||
if not fname.endswith(".jpg"):
|
||||
continue
|
||||
path = os.path.join(IMG_DIR, fname)
|
||||
img = Image.open(path)
|
||||
w, h = img.size
|
||||
img.close()
|
||||
|
||||
# Determine max size based on orientation
|
||||
if w > h:
|
||||
max_side = MAX_LANDSCAPE # landscape
|
||||
else:
|
||||
max_side = MAX_PORTRAIT # portrait or square
|
||||
|
||||
old_size, new_size, final_dims = optimize(path, max_side)
|
||||
total_old += old_size
|
||||
total_new += new_size
|
||||
saved_pct = (1 - new_size / old_size) * 100 if old_size > 0 else 0
|
||||
print(f" {fname:45s} {final_dims[0]:>5d}x{final_dims[1]:<5d} {old_size//1024:>4d}KB -> {new_size//1024:>4d}KB ({saved_pct:+.0f}%)")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Total: {total_old//1024}KB -> {total_new//1024}KB ({(1 - total_new/total_old)*100:.0f}% reduction)")
|
||||
print(f"{'='*60}")
|
||||
@@ -4,9 +4,12 @@
|
||||
"type": "module",
|
||||
"description": "Pi Coding Agent extension playground",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.78.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/cli": "^0.1.1"
|
||||
"@playwright/cli": "^0.1.1",
|
||||
"@types/better-sqlite3": "^7.6.13"
|
||||
}
|
||||
}
|
||||
|
||||
13
playwright-cli.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"browser": {
|
||||
"browserName": "chromium",
|
||||
"launchOptions": {
|
||||
"headless": true,
|
||||
"args": ["--ignore-certificate-errors"]
|
||||
},
|
||||
"contextOptions": {
|
||||
"ignoreHTTPSErrors": true,
|
||||
"viewport": { "width": 1440, "height": 900 }
|
||||
}
|
||||
}
|
||||
}
|
||||
5
pledge-now-pay-later/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
brand
|
||||
shots
|
||||
1
pledge-now-pay-later/.gitignore
vendored
@@ -41,3 +41,4 @@ next-env.d.ts
|
||||
# SQLite dev database
|
||||
*.db
|
||||
*.db-journal
|
||||
.pi/
|
||||
|
||||
333
pledge-now-pay-later/BRAND.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Pledge Now, Pay Later — Brand Guide
|
||||
|
||||
> **This is the single source of truth.** Every UI change, every new page, every generated asset must follow this document. When in doubt, refer to the visual assets in `brand/`.
|
||||
|
||||
---
|
||||
|
||||
## Brand Assets Location
|
||||
|
||||
All brand assets live in the `brand/` directory at the project root:
|
||||
|
||||
```
|
||||
brand/
|
||||
├── photography/ 20 landing page photos (Gemini 3 Pro)
|
||||
├── logo/ 6 logo variations
|
||||
├── color-palette/ 3 palette reference cards
|
||||
├── typography/ 3 type specimen sheets
|
||||
├── moodboard/ 3 visual direction boards
|
||||
├── brand-guide/ 7 brand guide pages
|
||||
├── social-templates/ 4 social media templates
|
||||
└── icons/ 6 line icons in brand colors
|
||||
```
|
||||
|
||||
Production images served from `public/images/landing/` (copied from `brand/photography/`).
|
||||
|
||||
### Asset Generation Rules
|
||||
- **Photography**: Generate with `gemini-3-pro-image-preview` (Nano Banana Pro). Documentary candid style. Save to `brand/photography/` AND `public/images/landing/`.
|
||||
- **Icons**: Line-only, 2px stroke, single brand color, white background. Save to `brand/icons/`.
|
||||
- **Social templates**: Use brand colors + Inter typography only. Save to `brand/social-templates/`.
|
||||
- **Any new visual asset**: Save to the appropriate `brand/` subfolder first, then copy to `public/` if needed for the website.
|
||||
|
||||
---
|
||||
|
||||
## Core Identity
|
||||
|
||||
**Name:** Pledge Now, Pay Later
|
||||
**Archetype:** The Steward — quiet authority, precision with warmth, trusted with money
|
||||
**Insight:** People don't break promises. Systems do.
|
||||
**Promise:** Every pledge tracked. Every donor reminded. Every penny accounted for.
|
||||
|
||||
### What We Are
|
||||
The missing infrastructure between "I'll donate" and the money arriving.
|
||||
|
||||
### What We Are Not
|
||||
- Not a payment processor
|
||||
- Not a CRM
|
||||
- Not a fundraising platform
|
||||
- Not a charity website builder
|
||||
|
||||
---
|
||||
|
||||
## Color System
|
||||
|
||||
Every color has a psychological job. No decorative color usage.
|
||||
|
||||
| Token | Hex | Name | Psychological Job | Use For |
|
||||
|-------|-----|------|-------------------|---------|
|
||||
| `midnight` | `#111827` | Midnight | Authority | Primary text, dark sections, logo, default buttons |
|
||||
| `promise-blue` | `#1E40AF` | Promise Blue | Action | Links, CTAs, active states, interactive elements |
|
||||
| `generosity-gold` | `#F59E0B` | Generosity Gold | Warmth | Pending states, highlights, volunteer accent |
|
||||
| `fulfilled-green` | `#16A34A` | Fulfilled Green | Completion | Paid badges, success states, confirmations |
|
||||
| `alert-red` | `#DC2626` | Alert Red | Urgency | Overdue, errors, needs-attention (never decorative) |
|
||||
| `paper` | `#F9FAFB` | Paper | Calm | Page backgrounds, alternating rows |
|
||||
|
||||
### Legacy Aliases (still in tailwind config)
|
||||
`trust-blue` = `promise-blue`, `warm-amber` = `generosity-gold`, `success-green` = `fulfilled-green`, `danger-red` = `alert-red`
|
||||
|
||||
### 60-30-10 Rule
|
||||
- **60%** Midnight + Paper (the base — dark text on light, or white text on dark)
|
||||
- **30%** Promise Blue (the action layer — everything interactive)
|
||||
- **10%** Gold + Green + Red (status indicators only)
|
||||
|
||||
### Reference
|
||||
- `brand/color-palette/01-primary-palette.jpg` — 6 color swatches
|
||||
- `brand/color-palette/02-tints-and-shades.jpg` — extended scales
|
||||
- `brand/color-palette/03-color-psychology.jpg` — psychological roles
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
**Font:** Inter (Google Fonts)
|
||||
**Import:** `@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap')`
|
||||
|
||||
| Level | Size | Weight | Tracking | Tailwind |
|
||||
|-------|------|--------|----------|----------|
|
||||
| Display | 72-96px | Black (900) | -0.025em | `text-7xl md:text-9xl font-black tracking-tighter` |
|
||||
| H1 | 48-60px | Black (900) | -0.02em | `text-4xl md:text-5xl font-black tracking-tight` |
|
||||
| H2 | 30-36px | ExtraBold (800) | -0.015em | `text-3xl font-black tracking-tight` |
|
||||
| H3 | 18-24px | Bold (700) | normal | `text-lg font-bold` or `text-base font-bold` |
|
||||
| Body | 14-16px | Regular (400) | normal | `text-sm` or `text-base` |
|
||||
| Caption | 11-12px | Medium (500) | 0.05em | `text-xs font-medium tracking-wide` |
|
||||
|
||||
### Numbered Steps Pattern
|
||||
Use `01`, `02`, `03` in Display/large size as visual anchors instead of icons:
|
||||
```jsx
|
||||
<p className="text-4xl font-black text-gray-200">01</p>
|
||||
```
|
||||
|
||||
### Reference
|
||||
- `brand/typography/01-typeface-specimen.jpg`
|
||||
- `brand/typography/02-heading-scale.jpg`
|
||||
- `brand/typography/03-numbers-in-brand.jpg`
|
||||
|
||||
---
|
||||
|
||||
## Logo
|
||||
|
||||
**Mark:** Solid square, zero border-radius, `bg-midnight`, white "P" in Inter Black.
|
||||
|
||||
```jsx
|
||||
{/* Standard logo mark */}
|
||||
<div className="h-7 w-7 bg-midnight flex items-center justify-center">
|
||||
<span className="text-white text-xs font-black">P</span>
|
||||
</div>
|
||||
|
||||
{/* With wordmark */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="h-7 w-7 bg-midnight flex items-center justify-center">
|
||||
<span className="text-white text-xs font-black">P</span>
|
||||
</div>
|
||||
<span className="font-black text-sm tracking-tight">Pledge Now, Pay Later</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Rules
|
||||
- Never round the mark corners
|
||||
- Never add shadows or gradients to the mark
|
||||
- Never use emoji (🤲) as a logo substitute
|
||||
- Minimum clear space = 1.5× height of mark on all sides
|
||||
- Three versions: dark on white, white on dark, blue mark on white
|
||||
|
||||
### Reference
|
||||
- `brand/logo/01-lockup-dark-on-white.jpg` — primary
|
||||
- `brand/logo/02-lockup-white-on-dark.jpg` — reversed
|
||||
- `brand/logo/03-lockup-blue-mark.jpg` — blue accent
|
||||
- `brand/logo/04-mark-dark.jpg` — mark only
|
||||
- `brand/logo/05-mark-blue.jpg` — blue mark only
|
||||
- `brand/logo/06-favicon.jpg` — edge-to-edge favicon
|
||||
|
||||
---
|
||||
|
||||
## Design Rules
|
||||
|
||||
### DO
|
||||
```
|
||||
✓ Sharp edges everywhere (rounded-lg max on interactive elements)
|
||||
✓ Typography as the hero — headlines readable without images
|
||||
✓ Numbered steps (01, 02, 03) instead of icons
|
||||
✓ Dark sections (bg-gray-950) for key stats and CTAs (max 2 per page)
|
||||
✓ border-l-2 accents for feature lists
|
||||
✓ gap-px grids for comparisons
|
||||
✓ Solid flat colors only
|
||||
✓ Generous whitespace
|
||||
```
|
||||
|
||||
### DO NOT
|
||||
```
|
||||
✗ rounded-2xl, rounded-3xl, rounded-full (except avatars/progress bars)
|
||||
✗ bg-gradient-to-* anything
|
||||
✗ shadow-lg shadow-{color}/25 (colored shadows)
|
||||
✗ backdrop-blur, glass effects
|
||||
✗ Emoji as visual anchors in headings
|
||||
✗ group-hover:scale-105 or any scale animations
|
||||
✗ animate-pulse-ring, animate-bounce-gentle (decorative animations)
|
||||
✗ More than 2 dark sections per page
|
||||
✗ Color on heading text — color goes on borders, badges, backgrounds
|
||||
✗ "AI-powered" as a feature name
|
||||
```
|
||||
|
||||
### Three Signature UI Patterns
|
||||
|
||||
**1. Left-Border Accent**
|
||||
```jsx
|
||||
<div className="border-l-2 border-midnight pl-4">
|
||||
<p className="text-sm font-bold text-midnight">Feature name</p>
|
||||
<p className="text-xs text-gray-500">Description text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**2. Gap-Px Grid**
|
||||
```jsx
|
||||
<div className="grid md:grid-cols-3 gap-px bg-gray-200">
|
||||
<div className="bg-white p-6">Cell 1</div>
|
||||
<div className="bg-white p-6">Cell 2</div>
|
||||
<div className="bg-white p-6">Cell 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**3. Dark Inversion**
|
||||
```jsx
|
||||
<section className="bg-gray-950 py-20 px-6">
|
||||
<h2 className="text-white font-black">Key stat or CTA</h2>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Reference
|
||||
- `brand/brand-guide/07-ui-patterns.jpg` — all three patterns visualized
|
||||
|
||||
---
|
||||
|
||||
## Button Variants
|
||||
|
||||
```jsx
|
||||
<Button variant="default"> {/* bg-midnight, white text */}
|
||||
<Button variant="blue"> {/* bg-promise-blue, white text */}
|
||||
<Button variant="success"> {/* bg-fulfilled-green, white text */}
|
||||
<Button variant="amber"> {/* bg-generosity-gold, white text */}
|
||||
<Button variant="destructive"> {/* bg-alert-red, white text */}
|
||||
<Button variant="outline"> {/* border, white bg */}
|
||||
<Button variant="ghost"> {/* transparent, hover gray */}
|
||||
<Button variant="link"> {/* promise-blue underline */}
|
||||
```
|
||||
|
||||
No button has colored shadows. No button has rounded corners beyond the base `--radius` (0.5rem).
|
||||
|
||||
---
|
||||
|
||||
## Photography Direction
|
||||
|
||||
**Style:** Documentary candid — never staged, never stock.
|
||||
|
||||
**Technical:**
|
||||
- Shallow depth of field (f/1.4–f/2.8)
|
||||
- Available/natural light (warm tungsten indoors, overcast outdoors)
|
||||
- Candid angles (never looking at camera)
|
||||
- British-diverse subjects (South Asian, Black British, Arab, white British)
|
||||
- Real settings (mosques, community centres, galas, homes, London streets)
|
||||
|
||||
**Never show:**
|
||||
- Stock handshakes
|
||||
- People pointing at screens and smiling
|
||||
- Overhead "team meeting" shots
|
||||
- Poverty imagery
|
||||
- Everyone looking at camera
|
||||
|
||||
**Generation prompt template:**
|
||||
```
|
||||
[Scene description]. [Subject description — ethnicity, age, clothing].
|
||||
[Setting — location, lighting]. [Camera — lens, f-stop, style].
|
||||
Shot on [camera], [focal length], f/[aperture], available light.
|
||||
[Mood]. [Aspect] aspect ratio.
|
||||
```
|
||||
|
||||
### Reference
|
||||
- `brand/photography/` — 20 generated photos
|
||||
- `brand/moodboard/01-trust-and-precision.jpg`
|
||||
- `brand/moodboard/02-community-and-giving.jpg`
|
||||
- `brand/brand-guide/06-photography-direction.jpg`
|
||||
|
||||
---
|
||||
|
||||
## Voice & Tone
|
||||
|
||||
### Voice (constant)
|
||||
- **Direct.** Short sentences. No filler.
|
||||
- **Specific.** Numbers, not vague claims. "60-second pledge flow" not "quick and easy."
|
||||
- **Empathetic.** We understand the awkwardness. Never shame.
|
||||
- **Confident.** "We fix that." Not "We can help with that."
|
||||
|
||||
### Words We Use
|
||||
| Use | Don't Use |
|
||||
|-----|-----------|
|
||||
| Pledge | Donation (for the promise stage) |
|
||||
| Campaign | Event (broader than physical events) |
|
||||
| Pledge link | QR code (QR is one delivery method) |
|
||||
| Reminder | Chaser, follow-up |
|
||||
| Conversion | Collection rate |
|
||||
|
||||
### Words We Never Use
|
||||
- "Revolutionary" / "game-changing" / "disruptive"
|
||||
- "Powered by AI"
|
||||
- "PNPL" in user-facing copy (internal + bank refs only)
|
||||
- "Simple" / "easy" (show, don't tell)
|
||||
|
||||
---
|
||||
|
||||
## Social Templates
|
||||
|
||||
- `brand/social-templates/01-og-image.jpg` — Open Graph (link previews)
|
||||
- `brand/social-templates/02-instagram-square.jpg` — Instagram post
|
||||
- `brand/social-templates/03-story-template.jpg` — WhatsApp/IG Story
|
||||
- `brand/social-templates/04-linkedin-banner.jpg` — LinkedIn company page
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
Line-only icons, 2px stroke, single brand color per icon:
|
||||
|
||||
| Icon | File | Color |
|
||||
|------|------|-------|
|
||||
| Pledge/Promise | `brand/icons/01-icon-pledge.jpg` | Midnight |
|
||||
| WhatsApp/Send | `brand/icons/02-icon-whatsapp.jpg` | Fulfilled Green |
|
||||
| Gift Aid | `brand/icons/03-icon-gift-aid.jpg` | Promise Blue |
|
||||
| Zakat | `brand/icons/04-icon-zakat.jpg` | Generosity Gold |
|
||||
| Dashboard | `brand/icons/05-icon-dashboard.jpg` | Midnight |
|
||||
| Schedule | `brand/icons/06-icon-schedule.jpg` | Midnight |
|
||||
|
||||
---
|
||||
|
||||
## CSS Variables
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%; /* white */
|
||||
--foreground: 222 47% 11%; /* midnight */
|
||||
--primary: 224 76% 40%; /* promise-blue */
|
||||
--primary-foreground: 0 0% 100%; /* white */
|
||||
--destructive: 0 72% 51%; /* alert-red */
|
||||
--border: 220 13% 91%; /* gray-200 */
|
||||
--radius: 0.5rem; /* max corner radius */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Allowed Animations
|
||||
|
||||
Only these — nothing decorative:
|
||||
|
||||
| Class | Use |
|
||||
|-------|-----|
|
||||
| `animate-fade-up` | Page sections appearing on load |
|
||||
| `animate-fade-in` | Elements becoming visible |
|
||||
| `animate-slide-down` | Dropdowns, notifications |
|
||||
| `animate-shimmer` | Loading skeleton states |
|
||||
| `animate-counter-roll` | Number counters |
|
||||
| `stagger-children` | Sequential card reveals |
|
||||
|
||||
---
|
||||
|
||||
*Brand Guide v1.0 — March 2026*
|
||||
*When adding a new page or component, re-read this file first.*
|
||||
@@ -1,9 +1,10 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
@@ -15,12 +16,14 @@ RUN npm run build
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
ENV NEXT_SHARP_PATH=/app/node_modules/sharp
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
|
||||
BIN
pledge-now-pay-later/brand/brand-guide/01-cover.jpg
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
pledge-now-pay-later/brand/brand-guide/02-brand-essence.jpg
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
pledge-now-pay-later/brand/brand-guide/03-logo-system.jpg
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
pledge-now-pay-later/brand/brand-guide/04-color-system.jpg
Normal file
|
After Width: | Height: | Size: 393 KiB |
BIN
pledge-now-pay-later/brand/brand-guide/05-typography-system.jpg
Normal file
|
After Width: | Height: | Size: 415 KiB |
|
After Width: | Height: | Size: 605 KiB |
BIN
pledge-now-pay-later/brand/brand-guide/07-ui-patterns.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
pledge-now-pay-later/brand/color-palette/01-primary-palette.jpg
Normal file
|
After Width: | Height: | Size: 335 KiB |