Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ jobs:
- name: Install dependencies
run: npm ci

# 2.x (OpenCode v2 only) ships under `next` so `latest` keeps serving 1.x to
# OpenCode v1 users. Promote later with `npm dist-tag add opencode-easy-vision@<version> latest`.
- name: Publish
run: npm publish --access public
run: npm publish --access public --tag next
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/
74 changes: 45 additions & 29 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,52 @@ 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; the last OpenCode v1 release is 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. End-to-end tests against the real OpenCode binary live in `test/e2e/` and run only through `just e2e`.

## 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 e2e # packed plugin against the installed OpenCode binary
```

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/config.ts](./src/config.ts): config discovery, 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 message rewriting that keeps part order 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/tool-route.ts](./src/tool-route.ts): whether the vision tool is reachable directly, through Code Mode's `execute`, or not at all.
- [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): per-path lock so image writes and cleanup never race.
- [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 +65,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. Act only when the request's model matches and the session belongs to this plugin instance's project.
2. Rewrite the outgoing model context only; never the stored chat history.
3. Save inline image data to disk; pass file and HTTP(S) URLs through. Fail with a clear reason for provider refs and unsupported formats.
4. Remove every image from user messages, even when saving fails. Keep all text and non-image media in place.
5. Add tool instructions only to the latest user message, for the route the tool is reachable on. Compaction, title and generate requests get image paths only.
6. Recreate saved files that have gone missing 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 content and media 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 +97,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: one OpenCode service hosts plugin instances for several projects. 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 e2e`.

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`. The publish workflow tags 2.x releases `next`, so `latest` keeps serving 1.x until the user decides to promote 2.x with `npm dist-tag`.

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