Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ permissions:

jobs:
ci:
name: Format & Build
name: Format, Build, Type-check & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -28,3 +28,9 @@ jobs:

- name: Build
run: npm run build

- name: Type-check tests
run: npm run typecheck

- name: Test with coverage thresholds
run: npm run test:coverage
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
.agents/
.vscode/
.DS_Store
coverage/
73 changes: 45 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,53 @@ Keep this file focused on maintainer instructions for coding agents. Do not copy
- User-facing setup and usage: [README.md](./README.md)
- Full plugin configuration reference: [CONFIGURATION.md](./CONFIGURATION.md)
- Agent-assisted user setup workflow: [AGENT_SETUP.md](./AGENT_SETUP.md)
- Local development and symlink testing: [CONTRIBUTING.md](./CONTRIBUTING.md)
- Local development and installed-package testing: [CONTRIBUTING.md](./CONTRIBUTING.md)
- Example config shipped with the package: [opencode-easy-vision.example.jsonc](./opencode-easy-vision.example.jsonc)

## Project Facts

- TypeScript, strict mode, ES modules only.
- Runtime target is Node.js `>=18.0.0`.
- Plugin framework is `@opencode-ai/plugin`.
- Runtime target is Node.js `>=22.18.0`.
- Targets OpenCode v2 only (tested on 2.1.4); v1 support ended with 1.6.1. Plugin framework is `@opencode/plugin` (Effect API); media/message types are from `@opencode/ai`.
- The only runtime import from the OpenCode ecosystem is `effect`, which the host resolves to its own instance. Import `@opencode/*` with `import type` only, so the published package needs no OpenCode dependencies.
- Source lives in `src/`; compiled output is `dist/`.
- There is no test framework. Validation is formatting plus TypeScript build, followed by manual OpenCode testing when behavior changes.
- Tests are TypeScript in `test/`, run by Vitest directly against `src/` (one `<module>.test.ts` per source module, shared fixtures in `test/helpers.ts`). Coverage thresholds in `vitest.config.ts` fail CI on regression. Runtime fixtures against a real OpenCode binary live in `scripts/v2-spike/`.

## Commands

Use [`just`](./justfile) for every task; run `just` to list recipes. The recipes wrap the npm scripts in `package.json`, which CI calls directly.

```bash
npm install
npm run format
npm run format:check
npm run build
just install # npm ci
just fmt # format src/ and test/
just build
just typecheck # type-check src/ and test/
just test # Vitest; extra args pass through, e.g. `just test config`
just coverage # tests plus coverage thresholds
just check # everything CI runs: fmt-check, build, typecheck, coverage
just runtime # end-to-end against the installed OpenCode binary
just runtime-pack # same, from an npm-packed tarball in a clean project
```

Use `npm run build` before committing code changes. CI runs `npm ci`, `npm run format:check`, and `npm run build`.
Run `just check` before committing code changes.

The Husky pre-commit hook runs `npm run format`. It only formats `src/**/*.ts`.
The Husky pre-commit hook runs `npm run format`, which formats `src/` and `test/`.

## Architecture

The plugin entry point is [src/index.ts](./src/index.ts). It registers `experimental.chat.messages.transform` and wires together the modules below.
The plugin entry point is [src/index.ts](./src/index.ts): a thin Effect shell that default-exports a typed `{ id, effect }` plugin object, routes logs to the host logger, loads config, runs cleanup, and registers the MCP transform plus the `context`, `compaction`, `title`, and `generate` hooks. Keep plugin logic out of Effect: hooks delegate to plain async code through `Effect.promise`.

- [src/constants.ts](./src/constants.ts): package constants, defaults, MIME mappings, prompt templates.
- [src/types.ts](./src/types.ts): shared interfaces and function types.
- [src/config.ts](./src/config.ts): config discovery, JSON/JSONC parsing, validation, precedence, example-config auto-init, and runtime accessors.
- [src/patterns.ts](./src/patterns.ts): case-insensitive wildcard model matching.
- [src/images.ts](./src/images.ts): image part detection, URL handling, temp-file writing, and image extraction.
- [src/images.ts](./src/images.ts): normalized image sources, URL handling, deterministic atomic temp-file writing.
- [src/prompt.ts](./src/prompt.ts): prompt-template substitution and default injection prompt selection.
- [src/transform.ts](./src/transform.ts): message lookup, model extraction, image-part removal, and text-part replacement/creation.
- [src/transform.ts](./src/transform.ts): immutable V2 content rewriting, preserving text/media ordering and metadata.
- [src/request.ts](./src/request.ts): per-request rewriting (model match, location guard, tool routing, transform, failure fallback) over Promise-based host lookups.
- [src/v2.ts](./src/v2.ts): request-tool exposure routing without permission bypasses.
- [src/codemode.ts](./src/codemode.ts): MCP transform that disables Code Mode for the server providing `imageAnalysisTool`, respecting explicit user settings.
- [src/file-lock.ts](./src/file-lock.ts): cross-instance write/cleanup coordination, never configuration storage.
- [src/cleanup.ts](./src/cleanup.ts): startup cleanup for plugin-owned temp files.

When adding behavior, preserve these module boundaries. Add a new small module for a new concern instead of growing [src/index.ts](./src/index.ts).
Expand All @@ -54,26 +66,26 @@ When adding behavior, preserve these module boundaries. Add a new small module f

The hook should stay defensive and non-crashing:

1. Find the last user message.
2. Read the model from that message and check it against configured patterns.
3. Extract supported image file parts.
4. Save `data:` images to the configured temp directory; pass `file://` and HTTP(S) images through as paths/URLs.
5. Remove all image parts that the target model cannot consume directly.
6. Inject or update text with the MCP tool instructions and the user's original request.
1. Match the active request model and verify the session belongs to this instance's location.
2. Normalize images across all retained user messages; never change durable attachments.
3. Save bytes/base64/data sources; pass through usable file/HTTP URLs. Fail explicitly for refs and unsupported formats.
4. Replace targeted raw media even on failure; retain unrelated content and all original text.
5. Add analysis guidance only for the current task, honoring direct/Code Mode exposure.
6. Keep historical/auxiliary image context neutral; recreate missing generated files on later passes.

If image processing partially or fully fails, log the failure, show a toast when the user can act on it, and keep OpenCode running.
If image processing fails, log through the injected `Logger`/`Notifier` (host Effect logger; OpenCode discards plugin `console` output) and add model-visible failure notes; the server has no terminal UI dependency. Do not log image bytes, secret URLs, credentials, or raw provider errors.

## Coding Rules

- Use `import type` for TypeScript-only imports.
- Use `node:` prefixes for Node built-ins.
- Keep import order as type imports, Node built-ins, external packages, then local modules.
- Do not use `require`, `module.exports`, `as any`, `@ts-ignore`, or `@ts-expect-error`.
- Prefer type guards for narrowing, especially around OpenCode `Part` variants.
- Prefer type guards for narrowing around V2 content/source variants.
- Prefer `??` for nullish defaults.
- Await filesystem operations and wrap I/O in `try`/`catch` when failure should not crash the plugin.
- Do not mutate parameters; return new data structures for message/part changes.
- Keep functions small and mostly pure. I/O and OpenCode logging/toasts are the expected side effects.
- Do not mutate parameters; return new data structures for message/part changes. The one exception is the OpenCode hook event or MCP editor, whose API contract is mutation: assign the newly built value once (for example `event.messages = ...`).
- Keep functions small and mostly pure. I/O and server logging are the expected side effects.
- Prefer Node built-ins over new dependencies unless the dependency removes real complexity.

## Configuration Changes
Expand All @@ -86,21 +98,26 @@ Configuration behavior is documented in [CONFIGURATION.md](./CONFIGURATION.md) a
- Update [CONFIGURATION.md](./CONFIGURATION.md) and the example JSONC file.
- Avoid duplicating option tables or examples in this file.

The module-level `pluginConfig` state in [src/config.ts](./src/config.ts) is intentional. Other modules should use exported accessors instead of threading config through every call.
Each adapter instance owns a `createConfigStore()` with isolated accessor closures. Bind accessors to helper factories once rather than threading options through each operation. Never restore module-global configuration: V2 locations coexist in one service. Inline host plugin options (`ctx.options`) are not read; config comes only from the dedicated files.

## Validation

For code changes, run:

```bash
npm run format:check
npm run build
just check
```

Run `npm run format` only when formatting changes are needed. For behavior that depends on OpenCode runtime wiring, follow the manual testing flow in [CONTRIBUTING.md](./CONTRIBUTING.md).
For changes to OpenCode runtime wiring (`src/index.ts`, hooks, packaging), also run `just runtime-pack`.

Every behavior change needs a test in the matching `test/<module>.test.ts`. Do not lower coverage thresholds to make CI pass. Tests may cast to `unknown` only where OpenCode types are impractical to construct (branded IDs, the full plugin `Context`); keep such casts in `test/helpers.ts` or one documented spot per file.

Run `just fmt` only when formatting changes are needed. For behavior that depends on OpenCode runtime wiring, follow the manual testing flow in [CONTRIBUTING.md](./CONTRIBUTING.md).

## Releases

Always confirm with the user before running any release command.

Use `npm version patch`, `npm version minor`, or `npm version major` to bump, commit, and tag. Push both the commit and tag so [.github/workflows/publish.yml](./.github/workflows/publish.yml) can publish to npm. Do not run `npm publish` manually.
2.x is OpenCode v2 only. Do not publish v1 fixes from `main`.

Release with `just release patch|minor|major`. It runs `just check`, then `npm version` (bump, commit, tag) and `git push --follow-tags`, so [.github/workflows/publish.yml](./.github/workflows/publish.yml) publishes to npm. The recipe asks for confirmation, but get the user's approval before running it. Do not run `npm publish` manually.
Loading
Loading