# SynthForge Pi extension

This package publishes the Pi layer for SynthForge, while the existing Python package remains the runtime. The npm candidate is `@superpapotas/synthforge`; the npm registry check returned 404 (not published at the time this package was prepared).

## Install in Pi

After publishing, install it with:

```text
pi install npm:@superpapotas/synthforge@0.1.0
```

For local development:

```text
pi -e ./dist/index.js
```

The package manifest explicitly loads `dist/index.js` through `pi.extensions`.

## Features

- `synthforge_models`: lists Pi's `modelRegistry.getAll()` and marks entries found by `getAvailable()`. It does not filter out `opencode` or `*:free` models.
- `synthforge_select_model` and `/synthforge-model`: open Pi's selector and call `pi.setModel`. OpenCode free entries remain selectable even before Pi marks the provider available; Pi remains the final authentication authority.
- `synthforge_run` and `/synthforge-run`: run a whitelisted SynthForge operation using either the optional requested Pi model or the active Pi model, with auth/base URL resolved by Pi for that exact model. Tool failures are thrown as Pi tool failures (the installed ExtensionAPI contract does not use a returned `isError` flag).
- `/synthforge-models`: displays the model catalog in a Pi widget.

Run parameters are JSON-shaped and support `augment-file`, `augment`, `run`, `estimate`, and `doctor`. File/table operations require their normal required fields (`inputPath`, `outputPath`, `columns`, `prompt`, or `sourceTable`/`targetTable`). Arbitrary subprocess commands are not accepted.

The optional `model` field accepts the natural Pi identifier `provider/model-id`, such as `opencode/deepseek-v4-flash-free`. A bare model id is also accepted when it is unique in `ctx.modelRegistry`. When `model` is present, the extension resolves it through Pi's registry, calls `pi.setModel` (and fails if Pi cannot activate it), then resolves credentials for that exact model. When `model` is omitted, the extension uses `ctx.model` and does **not** call `pi.setModel`.

Examples:

```text
/synthforge-run {"operation":"augment-file","inputPath":"products.csv","outputPath":"products_ai.csv","columns":"description VARCHAR","prompt":"Generate POS-ready copy","limit":5}
/synthforge-run {"operation":"doctor","model":"opencode/deepseek-v4-flash-free"}
```

The tool form uses the same field:

```json
{"operation":"augment-file","model":"opencode/deepseek-v4-flash-free","inputPath":"products.csv","outputPath":"products_ai.csv","columns":"description VARCHAR","prompt":"Generate POS-ready copy","limit":5}
```

## Python bridge contract (temporary CLI interface)

The extension passes values with Node's `child_process.spawn(..., { shell: false, env })`; it never builds a shell command, interpolates shell syntax, writes credentials to a file, or puts credentials or the Pi-only `model` selector in argv. Before applying the selected model configuration, it clears inherited bridge fields and always sets the selected provider/model/key/base URL values, so stale parent or project `.env` bridge values cannot override Pi's resolution. It inherits only the process/runtime settings needed by Python, SynthForge configuration, and the selected provider; unrelated host provider secrets are not copied.

The selected model is represented to the Python runtime as:

| Variable | Meaning |
| --- | --- |
| `SYNTHFORGE_PROVIDER` | Canonical SynthForge provider (`opencode`, `openrouter`, `anthropic`, `google`, or the original OpenAI-compatible provider) |
| `SYNTHFORGE_MODEL` | Provider-qualified model where SynthForge requires one (`openrouter/...`, `anthropic/...`, `gemini/...`, `azure/...`) |
| `SYNTHFORGE_BASE_URL` | Pi-resolved provider/model endpoint; OpenCode defaults to its OpenAI-compatible `/v1` endpoint |
| `SYNTHFORGE_API_KEY` | Pi-resolved API key, never logged by the extension |
| `SYNTHFORGE_API_VERSION` | Resolved provider API version when the selected completion route consumes one |
| `SYNTHFORGE_PROCESS_TIMEOUT_MS` | Optional child-process timeout (default 15 minutes, capped at 1 hour) |

The Python command is selected in this order. Probing and execution are direct argv calls with no shell; missing executables, cancellation, timeouts, and bounded output are reported with useful status details.

1. `SYNTHFORGE_EXECUTABLE` (direct CLI executable)
2. `SYNTHFORGE_PYTHON -m synthforge`
3. `python3 -m synthforge`, `python -m synthforge` (WSL/Linux)
4. `python.exe -m synthforge`, `py.exe -m synthforge`, `python -m synthforge` (Windows)
5. `synthforge`/`synthforge.exe` CLI

Each candidate is probed with `--version` and invoked directly with an argv array. Set `SYNTHFORGE_PYTHON` or `SYNTHFORGE_EXECUTABLE` when the Python installation is not on PATH. The runtime should be installed (`python -m pip install -e .` or a released SynthForge wheel) in the Pi working environment.

The Python package also exposes `Settings.with_pi_overrides(...)` for a future native bridge. Until a Python CLI/API entry point consumes that contract, this extension uses the environment contract above. The extension does not modify Python files.

## Provider/API handling

The temporary Python bridge supports only the protocols it can actually consume through its completion path: `openai-completions`, `anthropic-messages`, and `google-generative-ai`. OpenAI Responses, Azure Responses, Codex, Mistral, Bedrock, Pi Messages, and unknown API types fail clearly rather than being silently sent to the wrong protocol.

- Pi `opencode` models remain OpenCode models, including `opencode/deepseek-v4-flash-free` and other free IDs. Pi's `opencode-go` alias is normalized to `SYNTHFORGE_PROVIDER=opencode` because the Python contract recognizes only that provider name; the resolved endpoint and API key are preserved. This is the supported OpenCode free path.
- OpenAI-compatible completion models pass their resolved provider endpoint and API key to SynthForge/LiteLLM.
- Anthropic Messages and Google Generative AI are mapped to the corresponding LiteLLM model naming only when their protocol is supported by the bridge.
- Pi-resolved arbitrary custom HTTP headers are rejected rather than placed in an environment variable that the current Python bridge would ignore. Configure a provider with a normal API-key resolver or add a native bridge before using header-only auth.

Pi-resolved OAuth/API auth is queried at execution time for the requested/current model. API keys, bearer tokens, and credential environment values are redacted from returned process output and errors. Child stdout/stderr is capped (4 MiB combined by default), and Pi cancellation plus the bounded process timeout terminate the child. `SYNTHFORGE_ALLOW_PARTIAL`, rate limits, web search, and other normal SynthForge settings continue to come from the child environment/project `.env`.

## Development

```bash
npm ci
npm run check
npm test
npm run build
npm run pack:smoke
```

`prepare` and `prepack` build TypeScript automatically. To verify a clean publish artifact, remove `dist/` and run `npm pack --dry-run`; the prepack hook recreates `dist/` and the dry-run must list the compiled extension and declarations. No publish command is part of CI.

The tests cover model availability/OpenCode-free filtering, the exact public `model` contract including `opencode/deepseek-v4-flash-free`, natural and unambiguous model resolution, exact-model auth forwarding, explicit-only `pi.setModel` behavior, supported/unsupported provider mapping, safe argv/environment construction and redaction, bounded output and cancellation/timeouts, direct cross-platform process invocation, Google-compatible enum schemas, and command parameter validation.
