docs(agents): clean project instructions
This commit is contained in:
@@ -1,174 +1,80 @@
|
|||||||
# AGENTS.md — pi-perplexity
|
# pi-perplexity
|
||||||
|
|
||||||
## What This Is
|
## Stack
|
||||||
|
|
||||||
A **pi extension** (plugin for `@mariozechner/pi-coding-agent`) that provides web search via a Perplexity Pro/Max subscription. Uses OAuth JWT authentication against Perplexity's internal SSE endpoint — no API credits consumed, only the subscription.
|
- TypeScript extension for `@mariozechner/pi-coding-agent`; loaded directly via jiti, no build artifact.
|
||||||
|
- Node >=18.14.1 runtime APIs only: `fetch`, `crypto.randomUUID()`, `Buffer.from(..., "base64url")`, `Intl`.
|
||||||
|
- Pi-bundled packages live in `peerDependencies` with `"*"`: `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, `@sinclair/typebox`.
|
||||||
|
|
||||||
## Build / Test / Lint
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Type check
|
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
|
|
||||||
# Run tests
|
|
||||||
npm test
|
npm test
|
||||||
|
|
||||||
# Quick smoke test — factory returns valid tool shape
|
|
||||||
node --import @mariozechner/jiti/register --input-type=module -e "import('./src/index.ts').then((m)=>{ const entry = m.default?.default ?? m.default; if (typeof entry !== 'function') throw new Error('default export missing'); })"
|
node --import @mariozechner/jiti/register --input-type=module -e "import('./src/index.ts').then((m)=>{ const entry = m.default?.default ?? m.default; if (typeof entry !== 'function') throw new Error('default export missing'); })"
|
||||||
```
|
```
|
||||||
|
|
||||||
No build step. Extensions are loaded via jiti — TypeScript runs directly.
|
## File-Scoped Commands
|
||||||
|
|
||||||
## Project Structure
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Single test file | `npm run test:build && node --test --experimental-test-module-mocks ".tmp-test/test/search/stream.test.js"` |
|
||||||
|
| Live E2E opt-in | `PI_PERPLEXITY_E2E=1 npm test` |
|
||||||
|
|
||||||
```
|
## Structure
|
||||||
pi-perplexity/
|
|
||||||
package.json # pi extension manifest (see "omp"/"pi" field)
|
|
||||||
tsconfig.json
|
|
||||||
AGENTS.md # You are here
|
|
||||||
architecture.md # Full protocol spec — SSE format, auth flows, event schemas
|
|
||||||
plan.md # Implementation phases and acceptance criteria
|
|
||||||
docs/
|
|
||||||
pi_docs_extension.md # Official pi extension system documentation
|
|
||||||
pi_platform_reference.md # Pi platform reference (SDK, RPC, sessions, settings, packages)
|
|
||||||
src/
|
|
||||||
index.ts # CustomToolFactory entry — default export
|
|
||||||
auth/
|
|
||||||
jwt.ts # JWT base64url decode, expiry extraction
|
|
||||||
login.ts # macOS app extraction + email OTP flow
|
|
||||||
storage.ts # Token persistence (~/.config/pi-perplexity/auth.json)
|
|
||||||
search/
|
|
||||||
types.ts # All type definitions (StreamEvent, SearchResult, etc.)
|
|
||||||
client.ts # HTTP POST to SSE endpoint, orchestrates stream + merge
|
|
||||||
stream.ts # SSE line parser + incremental event merging
|
|
||||||
format.ts # SearchResult → LLM-readable text output
|
|
||||||
render/
|
|
||||||
call.ts # TUI renderCall component
|
|
||||||
result.ts # TUI renderResult component
|
|
||||||
```
|
|
||||||
|
|
||||||
## Critical Constraints
|
- `src/index.ts` — extension entry and `perplexity_search` tool definition.
|
||||||
|
- `src/auth/` — JWT extraction, email OTP login, token storage.
|
||||||
|
- `src/search/` — Perplexity SSE request, stream merge, result formatting, models.
|
||||||
|
- `src/render/` — TUI renderers for tool calls and results.
|
||||||
|
- `src/commands/` — slash commands such as login/config.
|
||||||
|
- `test/` — automated Node test suite; `.tmp-test/` is generated by `npm run test:build`.
|
||||||
|
- `local_tests/` — manual/debug scripts and captured dumps; do not treat as automated coverage.
|
||||||
|
- `docs/design-decisions.md` — rationale for non-obvious behavior.
|
||||||
|
|
||||||
### Zero Runtime Dependencies
|
## Extension Conventions
|
||||||
This plugin has **zero npm dependencies**. All HTTP, SSE parsing, JWT decoding, and UUID generation use platform globals:
|
|
||||||
- `fetch` — global (Node 18.14.1+ for `Headers.getSetCookie()` support)
|
|
||||||
- `crypto.randomUUID()` — global
|
|
||||||
- `atob` / `Buffer.from(payload, "base64url")` — global
|
|
||||||
- `Intl.DateTimeFormat` — global
|
|
||||||
|
|
||||||
Do NOT add dependencies to `dependencies` in package.json.
|
- Entry point exports a default factory from `src/index.ts`.
|
||||||
|
- Tool name is `perplexity_search`.
|
||||||
|
- Tool `execute` signature is `(toolCallId, params, signal, onUpdate, ctx)`.
|
||||||
|
- Tool results use `{ content: [{ type: "text", text }], details: { ... } }`.
|
||||||
|
- String enum params use `StringEnum` from `@mariozechner/pi-ai`; `Type.Union([Type.Literal(...)])` breaks Google models.
|
||||||
|
- Stream/API types stay loose: all reverse-engineered event fields are optional.
|
||||||
|
|
||||||
### Peer Dependencies Only
|
## Perplexity Protocol Gotchas
|
||||||
These are bundled by pi and must go in `peerDependencies` with `"*"` range:
|
|
||||||
- `@sinclair/typebox` — schema definitions (injected at runtime via `api.typebox`)
|
|
||||||
- `@mariozechner/pi-tui` — TUI Component types for renderers
|
|
||||||
|
|
||||||
### Reverse-Engineered API
|
- Endpoint: `POST https://www.perplexity.ai/rest/sse/perplexity_ask`.
|
||||||
The Perplexity SSE endpoint is **not a public API**. It can break without notice.
|
- Required Perplexity constants live in `src/constants.ts`.
|
||||||
- Keep types loose — all fields optional
|
- Searches default to `is_incognito: true` to avoid polluting user history.
|
||||||
- Keep the client thin — minimal assumptions about response shape
|
- The response is `data:` JSON lines with `[DONE]`, not a fully standard SSE stream.
|
||||||
- Specific User-Agent and headers are required (see architecture.md § Request)
|
- Events are incremental snapshots: shallow-merge top level, merge blocks by `intended_usage`, splice markdown via `chunk_starting_offset`, accumulate sources.
|
||||||
- `is_incognito: true` always — don't pollute user's Perplexity history
|
- Completion is `event.final === true` or `event.status === "COMPLETED"`.
|
||||||
|
- Answer extraction order: markdown blocks → `ask_text` blocks → event text.
|
||||||
|
- Source extraction order: `web_results` block → `sources_list`; deduplicate by URL.
|
||||||
|
|
||||||
## Coding Conventions
|
## Auth Gotchas
|
||||||
|
|
||||||
### TypeScript
|
- Auth tries macOS app token first via `defaults read ai.perplexity.mac authToken`, unless `PI_AUTH_NO_BORROW=1`.
|
||||||
- `strict: true`, `noEmit: true`
|
- Email OTP fallback uses `ctx.ui.input()`.
|
||||||
- Target: ESNext, module: ESNext, moduleResolution: bundler
|
- Tokens are stored at `~/.config/pi-perplexity/auth.json` with mode `0600`.
|
||||||
- Use `interface` for data shapes, `type` for unions/aliases
|
- JWT expiry is `decoded.exp * 1000` with a 5 minute buffer; decode failures fall back to 1 hour.
|
||||||
- All stream event fields are optional — the API is unstable
|
- AUTH errors preserve the cached token; users explicitly clear/re-auth with `/perplexity-login --force`.
|
||||||
|
|
||||||
### Extension API
|
## Boundaries
|
||||||
- Entry point: `src/index.ts` exports `default` function or factory
|
|
||||||
- Import types from `@mariozechner/pi-coding-agent`
|
|
||||||
- Use `StringEnum` from `@mariozechner/pi-ai` for string enum params — `Type.Union`/`Type.Literal` breaks Google's API
|
|
||||||
- Tool execute signature: `execute(toolCallId, params, signal, onUpdate, ctx)`
|
|
||||||
- Return shape: `{ content: [{ type: "text", text }], details: { ... } }`
|
|
||||||
|
|
||||||
### Naming
|
### Always
|
||||||
- Tool name: `perplexity_search` (snake_case, matches pi convention)
|
|
||||||
- File names: lowercase, descriptive (`stream.ts`, `client.ts`, `jwt.ts`)
|
|
||||||
- Types: PascalCase (`StreamEvent`, `SearchResult`, `StoredToken`)
|
|
||||||
- Constants: UPPER_SNAKE or camelCase for compound values
|
|
||||||
|
|
||||||
### Error Handling
|
- Pass `AbortSignal` through to network calls.
|
||||||
- Never throw raw errors from tool execute — always return error text in `content`
|
- Return user-readable error text from tool execution instead of throwing raw errors.
|
||||||
- Use a `SearchError` class for typed errors with code and message
|
- Keep runtime dependency-free; use platform APIs for HTTP, SSE parsing, JWT decoding, and UUIDs.
|
||||||
- HTTP 401/403: clear stored token, return auth error to agent
|
|
||||||
- HTTP 429: return rate limit message with retry suggestion
|
|
||||||
- Network failures: return error text, don't crash
|
|
||||||
- Empty responses: return "No results found"
|
|
||||||
- JWT errors: fallback expiry of 1 hour if decode fails
|
|
||||||
|
|
||||||
## Auth Flow (two paths, tried in order)
|
### Ask First
|
||||||
|
|
||||||
1. **macOS Desktop App** (zero-interaction): `defaults read ai.perplexity.mac authToken`
|
- Changing Perplexity request headers, model IDs, or auth flow behavior.
|
||||||
- Skip if `PI_AUTH_NO_BORROW=1` is set
|
- Adding package dependencies or generated build artifacts.
|
||||||
- Returns null on non-macOS or if app not installed
|
- Deleting captured local test dumps or token-related files.
|
||||||
2. **Email OTP** (interactive fallback): CSRF → send OTP → verify OTP
|
|
||||||
- Uses `ctx.ui.input()` for OTP prompt
|
|
||||||
|
|
||||||
Token stored at `~/.config/pi-perplexity/auth.json` with `0600` permissions.
|
### Never
|
||||||
JWT expiry: `decoded.exp * 1000 - 5min` buffer. No auto-refresh — re-login on expiry.
|
|
||||||
|
|
||||||
## SSE Stream Protocol
|
- Never commit secrets, credentials, `.env`, cached auth tokens, or live Perplexity response dumps containing private data.
|
||||||
|
- Never clear cached auth automatically on transient 401/403 responses.
|
||||||
Endpoint: `POST https://www.perplexity.ai/rest/sse/perplexity_ask`
|
|
||||||
|
|
||||||
Events are **incremental snapshots** that must be merged:
|
|
||||||
- Top-level fields: shallow merge
|
|
||||||
- Blocks: keyed by `intended_usage` — merge, don't replace array
|
|
||||||
- Markdown chunks: respect `chunk_starting_offset` for splice
|
|
||||||
- Sources: accumulate, never replace; preserve from earlier events
|
|
||||||
|
|
||||||
Stream terminates when `event.final === true` or `event.status === "COMPLETED"`.
|
|
||||||
|
|
||||||
Answer extraction priority: markdown blocks → ask_text blocks → event.text fallback.
|
|
||||||
Source extraction priority: web_results block → sources_list fallback. Deduplicate by URL.
|
|
||||||
|
|
||||||
Full protocol details: `architecture.md` § Search Protocol, § Response: SSE Event Stream.
|
|
||||||
|
|
||||||
## Tool Output Format
|
|
||||||
|
|
||||||
```
|
|
||||||
## Answer
|
|
||||||
<synthesized answer>
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
N sources
|
|
||||||
[1] Title (2d ago)
|
|
||||||
https://url
|
|
||||||
snippet preview...
|
|
||||||
|
|
||||||
## Meta
|
|
||||||
Provider: perplexity (oauth)
|
|
||||||
Model: <display_model>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Age: human-readable relative time ("2d ago", "3h ago", "just now")
|
|
||||||
- Snippets: truncated to 240 chars
|
|
||||||
- Source count: respect `limit` parameter
|
|
||||||
|
|
||||||
## Key References
|
|
||||||
|
|
||||||
| What | Where |
|
|
||||||
|------|-------|
|
|
||||||
| Full protocol spec (headers, body, SSE events) | `architecture.md` |
|
|
||||||
| Implementation phases and acceptance criteria | `plan.md` |
|
|
||||||
| Pi extension system overview | `docs/pi_docs_extension.md` |
|
|
||||||
| Pi platform reference (SDK, RPC, sessions, settings, packages) | `docs/pi_platform_reference.md` |
|
|
||||||
|
|
||||||
## Design Decisions
|
|
||||||
|
|
||||||
See `docs/design-decisions.md` for rationale on non-obvious choices.
|
|
||||||
|
|
||||||
## Common Gotchas
|
|
||||||
|
|
||||||
- `Type.Union([Type.Literal("a"), ...])` does NOT work for Google models — use `StringEnum` from `@mariozechner/pi-ai`
|
|
||||||
- Tool `execute` param order is `(toolCallId, params, signal, onUpdate, ctx)` — signal before onUpdate
|
|
||||||
- The SSE stream is NOT standard Server-Sent Events — it uses `data:` lines with JSON but requires custom parsing for multi-line data fields and `[DONE]` marker
|
|
||||||
- Perplexity SSE events are incremental snapshots, not deltas — each event contains the full state up to that point, but blocks must still be merged by `intended_usage` key
|
|
||||||
- macOS `defaults read` via `node:child_process` — handle non-zero exit code (app not installed) gracefully
|
|
||||||
- JWT `exp` claim is in seconds, not milliseconds — multiply by 1000
|
|
||||||
- Token file must be written with `0600` permissions — use `node:fs/promises` and enforce mode on existing files
|
|
||||||
- Always pass `AbortSignal` through to `fetch` for cancellation support
|
|
||||||
|
|||||||
Reference in New Issue
Block a user