@mortz.prk/opencode-auto-mode 1.0.2 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,336 +1,221 @@
1
1
  # OpenCode Auto Mode
2
2
 
3
- Reviews OpenCode permission prompts automatically. OpenCode decides what needs
4
- approval; the plugin reviews only `ask` actions.
3
+ Automatic review of OpenCode permission requests. While a session's auto mode is
4
+ on, the plugin reviews native **allow** and **ask** effects, including saved
5
+ **Always** approvals. A native **deny** is always final.
5
6
 
6
- ## Install and configure
7
+ Auto Mode reduces prompts. It is not a security sandbox, and it cannot make an
8
+ invalid configuration fail closed.
7
9
 
8
- Add this to `~/.config/opencode/opencode.jsonc`. It asks for permission on every
9
- action and reviews those requests using built-in checks. No model or API key is
10
- needed:
10
+ ## Requirements
11
11
 
12
- ```jsonc
13
- {
14
- "$schema": "https://opencode.ai/config.json",
15
- "permissions": [
16
- { "action": "*", "resource": "*", "effect": "ask" }
17
- ],
18
- "plugins": [
19
- {
20
- "package": "@mortz.prk/opencode-auto-mode",
21
- "options": {
22
- "classifiers": [
23
- { "type": "static" }
24
- ]
25
- }
26
- }
27
- ]
28
- }
29
- ```
30
-
31
- OpenCode watches configuration files and reloads plugins after a save, so no
32
- restart is needed. If a change does not take effect, run `/reload` in the TUI to
33
- force a full reload.
34
-
35
- There is no default review pipeline, and empty options are invalid. Add at least
36
- one review entry. Built-in checks deny dangerous shell commands, allow common
37
- read-only commands, and send other parsed commands to the configured reviewers.
38
- Dynamic syntax such as command substitution, nested shells, and stateful builtins
39
- still requires manual approval.
40
-
41
- The snippets below change parts of that configuration; keep your `classifiers`
42
- list when adding other options.
43
-
44
- ### How a shell command is classified
12
+ - OpenCode V2. The checked-in plugin SDK and schema targets OpenCode `2.2.0`.
13
+ - Model references only if you use `decision` or `llm` classifier stages. Models
14
+ and provider credentials belong in your personal OpenCode provider config.
15
+ This repository supplies no model, endpoint, credential, or allowlist.
16
+ - **Data egress:** a configured `decision` or `llm` stage sends the action and
17
+ resources (including full Code Mode source), agent, project/worktree, git
18
+ state, and up to 12 KB of user messages to that model's provider, even with
19
+ audit off. Choose a provider you trust.
45
20
 
46
- Commands are parsed before they are judged, so text that merely mentions a
47
- dangerous word — a commit message, a search pattern, a heredoc body — is not
48
- treated as a dangerous operation. Denials apply to the parsed executable and its
49
- arguments, and nested shells are inspected recursively.
21
+ ## Install
50
22
 
51
- Read-only allowlisting checks the executable **and** its flags. Options that
52
- write files or run programs (`rg --pre`, `sort -o`, `tree -o`, `file -C`,
53
- `bat --pager`, `hostname NAME`, `uniq IN OUT`) are handed to the reviewers
54
- instead of being auto-approved.
23
+ Register the plugin **once** in your user-owned global config:
55
24
 
56
- A shell result is final when it is `deny` or `ask`: `ask` means manual approval,
57
- even if a model reviewer is configured. Read-only auto-approval is narrower than
58
- the pipeline: for shell actions, the static stage auto-approves only commands the
59
- guard recognizes as read-only. Every other parsed command that is not denied —
60
- including a write such as `pwd > marker` — goes to the configured reviewers, and a
61
- model reviewer may allow it. With no reviewer configured, it stays `ask`.
62
-
63
- ## Add a typed decision model
25
+ `~/.config/opencode/opencode.jsonc` (or
26
+ `$XDG_CONFIG_HOME/opencode/opencode.jsonc`). The global config must be outside
27
+ the repository, resolve with no symlink in its path, be user-owned and not
28
+ group/other-writable, be a single hardlink, and use an absolute
29
+ `XDG_CONFIG_HOME` when set.
64
30
 
65
31
  ```jsonc
66
32
  {
67
- "options": {
68
- "classifiers": [
69
- { "type": "static" },
70
- { "type": "decision", "model": "typesafe-ai/jev-latest" }
71
- ]
72
- }
73
- }
74
- ```
75
-
76
- | Provider | Setup |
77
- | --- | --- |
78
- | `typesafe-ai` | Set `TYPESAFE_API_KEY`. |
79
- | `openai` | Set `OPENAI_API_KEY`. |
80
- | `laya` | Run a loopback `/v1/systemone` service. `LAYA_API_KEY` is optional. |
81
-
82
- Decision entries accept `minConfidence` (default `0.9`), `timeoutMs`, and
83
- `apiKeyEnv`. `laya` also accepts `endpoint`.
84
-
85
- ## Add an LLM
86
-
87
- Add an `llm` entry after the built-in checks:
88
-
89
- ```jsonc
90
- {
91
- "options": {
92
- "classifiers": [
93
- { "type": "static" },
94
- { "type": "llm" }
95
- ]
96
- }
97
- }
98
- ```
99
-
100
- When an `llm` entry has no `model`, the **session model** is used. To choose a
101
- different model:
102
-
103
- ```jsonc
104
- { "type": "llm", "model": "anthropic/claude-sonnet-4-5" }
105
- ```
106
-
107
- ## Combine reviewers
108
-
109
- Entries run in order. Add as many as you need:
110
-
111
- ```jsonc
112
- {
113
- "options": {
114
- "classifiers": [
115
- { "type": "static" },
116
- { "type": "decision", "model": "typesafe-ai/jev-latest" },
117
- { "type": "llm" }
118
- ]
119
- }
120
- }
121
- ```
122
-
123
- An entry can allow, deny, or ask, which stops the pipeline. An entry that abstains
124
- hands the action to the next entry. If nothing decides, the action remains `ask`.
125
-
126
- ## Time limit and failures
127
-
128
- A review never blocks a permission prompt indefinitely. One deadline covers every
129
- entry in the pipeline, including both model calls of an `llm` entry:
130
-
131
- ```jsonc
132
- { "options": { "reviewTimeoutMs": 60000 } }
133
- ```
134
-
135
- `reviewTimeoutMs` accepts 1000–120000 and defaults to 60000. When the deadline
136
- passes, provider and context requests are aborted and the action is asked with
137
- `Auto-mode review deadline exceeded`. Interrupting a session also abandons its
138
- review, so a stopped turn cannot later receive an automatic approval.
139
-
140
- Failures are reported with a bounded cause — `deadline`, `timeout`, `cancelled`,
141
- `provider`, `invalid-output`, or `context` — in the audit log and in OTLP
142
- (`error.kind`), while the user-facing message stays generic.
143
-
144
- A typed decision reviewer that is unavailable or returns a malformed response
145
- abstains, so the next entry runs. An `llm` entry behaves differently: a timeout
146
- or provider error abstains, but an unparseable final answer is an `ask` (manual
147
- approval) with `invalid-output`, and the pipeline stops there.
148
-
149
- ## Use it for specific agents
150
-
151
- OpenCode allows everything by default. To review only the `build` agent, set its
152
- permissions and leave global permissions unchanged:
153
-
154
- ```jsonc
155
- {
156
- "agents": {
157
- "build": {
158
- "permissions": [
159
- { "action": "*", "resource": "*", "effect": "ask" }
33
+ "$schema": "https://opencode.ai/config.json",
34
+ "plugins": [{
35
+ "package": "@mortz.prk/opencode-auto-mode",
36
+ "options": {
37
+ "classifiers": [
38
+ { "type": "static" },
39
+ { "type": "llm", "model": "<provider>/<reviewer-model>" }
160
40
  ]
161
41
  }
162
- }
42
+ }]
163
43
  }
164
44
  ```
165
45
 
166
- ## TUI indicator
46
+ The `static` stage handles low-risk reads and eligible in-project edits without a
47
+ model. Remove the `llm` stage if you want a static-only setup; anything static
48
+ cannot resolve then asks you.
167
49
 
168
- A passive indicator shows when the plugin is reviewing the current session.
169
- It is enabled by default. Set `tui: false` in plugin options to hide it.
170
- Invalid options show `auto-mode: invalid config` in the same place and raise an
171
- error toast when the configuration is loaded. OpenCode's own permission dialog
172
- handles manual approvals.
50
+ For native typed decisions, add a `decision` stage between `static` and `llm`:
173
51
 
174
- The indicator window is longer than the maximum review deadline, so a slow
175
- review stays visible until it resolves.
52
+ ```jsonc
53
+ { "type": "decision", "model": "<provider>/<decision-model>", "minConfidence": 0.9 }
54
+ ```
176
55
 
177
- ## Desktop app (preview)
56
+ Only add this stage if your OpenCode provider supports native decisions. The
57
+ plugin has no HTTP decision adapter or custom endpoint option.
178
58
 
179
- OpenCode 2.2.0+ can load the plugin's GUI entry in the desktop app. It adds a
180
- composer pill and an **Auto-mode** side panel showing whether the current session
181
- is being reviewed and how recent actions were decided, plus a command-palette
182
- entry to open the panel.
59
+ Keep the plugin registration in global config only. Do not add the full plugin to
60
+ project config, `cli.json`, or `.opencode/plugins/` as well: the plugin reads its
61
+ safety options from exactly one global registration. The server registration
62
+ loads the TUI part automatically. Desktop loads the experimental GUI part from
63
+ global config and needs the built `dist/gui-experimental.cjs` bundle; in a local
64
+ checkout, run `bun run build:gui` first.
183
65
 
184
- The GUI only reads state. Every permission decision still comes from the server
185
- part, so disabling the GUI never disables automatic review.
66
+ ## Verify
186
67
 
187
- The GUI is loaded from the desktop app's own background server and reads that
188
- server's auto-mode state. If you connect the app to another server, that server's
189
- reviews do not appear in the panel.
68
+ Restart nothing for a config-only change. OpenCode watches config files and
69
+ reloads plugins after a save; `/reload` forces a full reload if needed.
190
70
 
191
- The desktop app reads plugins only from the **global** `opencode.json(c)`. If
192
- your TUI configuration lives in `cli.json`, add the package to the global file as
193
- well:
71
+ Run:
194
72
 
195
- ```jsonc
196
- {
197
- "plugins": [{ "package": "@mortz.prk/opencode-auto-mode", "options": { "classifiers": [{ "type": "static" }] } }]
198
- }
73
+ ```text
74
+ /auto-mode status
199
75
  ```
200
76
 
201
- Turn the GUI off without removing the plugin in **Settings → Extensions →
202
- Plugins**. The GUI plugin API is experimental and may change between OpenCode
203
- releases; the server and TUI parts do not depend on it.
77
+ A working install reports `Auto mode: on`, today's allow/ask/deny counts, active
78
+ reviews, and breaker state. With invalid configuration, the command reports
79
+ `Auto mode: invalid` and `Review disabled: <error>`; the desktop pill reads
80
+ `Invalid config — review disabled`. A storage or status failure is not a config
81
+ error: the command appends its error and the pill reads `Review unavailable` or
82
+ `status unavailable`. Fix the strict option shapes below rather than expecting a
83
+ migration hint.
204
84
 
205
- ## Review options
85
+ ## Use
206
86
 
207
- Each LLM or typed decision reviewer can have its own policy:
87
+ - `/auto-mode` toggles the current session.
88
+ - `/auto-mode on` enables review and resets the breaker.
89
+ - `/auto-mode off` restores the native host effect.
90
+ - `/auto-mode status` reports mode, counts, active reviews, and breaker state.
208
91
 
209
- ```jsonc
210
- {
211
- "options": {
212
- "classifiers": [
213
- {
214
- "type": "llm",
215
- "policy": {
216
- "trusted": ["the repository and its CI configuration"],
217
- "blocks": ["publish packages", "change production infrastructure"],
218
- "exceptions": ["run the repository's test and lint scripts"]
219
- }
220
- }
221
- ]
222
- }
223
- }
224
- ```
92
+ Configure native permissions for ordinary safe work and leave residual risky
93
+ operations as `ask`. Keep hard denials in user-owned global `policies`
94
+ (`tool.use`); the plugin does not install or rewrite native rules.
225
95
 
226
- `trusted` describes the trusted boundary, `blocks` describes actions to deny, and
227
- `exceptions` describes narrow acceptable cases. Omitted lists use the built-in
228
- policy; an empty list clears that list. Policies are independent between
229
- reviewers and do not apply to static checks or replace OpenCode permissions.
96
+ ## Options
230
97
 
231
- ## All plugin options
98
+ Unknown keys are rejected. `classifiers` is required and must contain 1–64
99
+ ordered stages, with at most one `static` stage.
232
100
 
233
- | Option | Default | Meaning |
101
+ | Option | Default | Contract |
234
102
  | --- | --- | --- |
235
- | `classifiers` | — | Ordered review entries; at least one is required. |
236
- | `reviewTimeoutMs` | `60000` | Deadline for one complete review (1000–120000). |
237
- | `auditLog` | off | `true` writes the default JSONL file; a string sets the path. |
238
- | `otlp` | off | `true` uses OpenCode's OTLP environment; an object sets `endpoint`, `headers`, and `timeoutMs`. |
239
- | `tui` | on | Set `false` to hide the TUI indicator. |
240
-
241
- Static entries accept `allowInProjectEdits`. Decision entries accept
242
- `minConfidence`, `timeoutMs`, `apiKeyEnv`, and `endpoint` (loopback `laya` only;
243
- defaults to `http://127.0.0.1:8000/v1/systemone`). OpenCode reloads the plugin
244
- when you save, so option changes apply without a restart; run `/reload` in the
245
- TUI if a change does not take effect. Invalid options disable automatic approval
246
- and are reported in the TUI and over the status RPC.
247
-
248
- ## Data that leaves the machine
249
-
250
- Reviewers see the proposed action, its resources, the session directory, the
251
- requesting agent, the pending permission `metadata` and `source`, and a bounded
252
- transcript of recent user messages and tool inputs (12 KB, with the head and tail
253
- of each tool input). Nothing else about the session is sent.
254
-
255
- * A model reviewer receives that material through OpenCode's own session model or
256
- the `model` you configure.
257
- * A typed decision reviewer receives it at the configured provider endpoint.
258
- * Audit records and OTLP log records keep the action, agent, up to 16 resources,
259
- a short reason, and, on failure, a short error detail. Both pass the record
260
- through one sanitizer, which bounds these fields — 128 characters for the
261
- action and agent, 64 for the stage, 512 for each resource, reason, and error
262
- detail — but does **not** redact text.
263
- * OTLP exports logs and traces as separate streams (`/v1/logs` and `/v1/traces`).
264
- Trace root attributes use the same sanitizer, so their action, agent, and stage
265
- are bounded like the log fields. Each reviewer attempt adds a child span with
266
- its model and outcome; the `model` attribute is not clipped. Traces do not
267
- include resources, reasons, or error-detail text.
268
- * Text in every stream is unredacted, including the action and agent. Traces
269
- carry the action and agent; logs carry those plus resources, reasons, and
270
- error details. These fields may be sensitive, and resources or reasons can
271
- contain inline file contents or shell commands, including credentials such as
272
- an `Authorization` header. Audit JSONL keeps the same unredacted fields as OTLP
273
- logs. Enable `auditLog` or `otlp` only if you are comfortable storing or
274
- exporting that material.
275
- * Records do not include the reviewer prompt, the session transcript, or API keys
276
- configured for providers.
277
-
278
- ## Platform limits
279
-
280
- * Windows always asks for shell actions; the guard only parses POSIX syntax.
281
- * Reviews happen inside the OpenCode server, so a headless or remote TUI behaves
282
- the same as a local one. The desktop GUI needs OpenCode 2.2.0+.
283
-
284
- ## Auditing
285
-
286
- Enable the audit log in plugin options:
287
-
288
- ```jsonc
289
- {
290
- "options": {
291
- "auditLog": true
292
- }
293
- }
294
- ```
295
-
296
- Decisions are appended as JSON lines to
297
- `$XDG_STATE_HOME/opencode-auto-mode/decisions.jsonl`. Replay recorded tool calls
298
- without executing anything (run from a checkout; it reads the OpenCode database
299
- read-only and needs `--config` if your global config is not at a default path):
103
+ | `classifiers` | required | Ordered `static`, `decision`, and `llm` stages. No default list and no unconfigured mode. |
104
+ | `reviewTimeoutMs` | `15000` | Whole-review deadline; integer 1–120000. |
105
+ | `breaker` | `3 / 10 / 50` | `{consecutiveDenies, windowDenies, windowSize}`. All three keys are required when supplied. |
106
+ | `tui` | `true` | Show or hide the TUI indicator. UI-only; review still runs server-side. |
107
+ | `audit` | `{enabled:false,maxBytes:10485760}` | Object only. Local audit is off by default. |
300
108
 
301
- ```sh
302
- bun scripts/replay.ts [--days 14] [--session ID] [--agent build|any] [--json]
303
- ```
304
-
305
- To send decisions through OpenCode's OTLP telemetry, configure OTLP for OpenCode
306
- with its standard environment variables, then enable the plugin flag. This sends
307
- correlated decision logs and review traces, including model reviewer attempts:
109
+ Stage shapes:
308
110
 
309
- ```jsonc
310
- {
311
- "options": {
312
- "otlp": true
313
- }
314
- }
315
- ```
316
-
317
- ## Development
318
-
319
- ```sh
320
- bun install
321
- bun test
322
- bun run lint
323
- bun run typecheck
324
- bun run build:gui
325
- bun run test:integration
111
+ | Stage | Shape |
112
+ | --- | --- |
113
+ | `static` | `{ "type": "static", "allowInProjectEdits"?: boolean }` |
114
+ | `decision` | `{ "type": "decision", "model": "<provider>/<model>", "timeoutMs"?: number, "minConfidence"?: number }` |
115
+ | `llm` | `{ "type": "llm", "model": "<provider>/<model>", "timeoutMs"?: number }` |
116
+
117
+ ## How review works
118
+
119
+ 1. Native permissions, policies, and saved approvals produce `allow`, `ask`, or
120
+ `deny`. A native **deny is final** and is never reviewed or weakened.
121
+ 2. With mode on, native allows and asks are reviewed. Saved **Always** entries
122
+ are re-reviewed but never deleted or rewritten.
123
+ 3. Built-in guards run first: protected paths and credentials, risky shell
124
+ structure, recursive external deletion, history changes, and ambiguous syntax.
125
+ These guards are not user-extensible. Git metadata (`.git`), Git global
126
+ options, and complex POSIX shell syntax are left to the configured decision/LLM
127
+ stages, not automatically denied or forced to a user prompt. Credential
128
+ protections and visible blocked operations still take precedence. Unsupported
129
+ Windows shell dialects still require manual approval. Loading a skill is
130
+ low-risk: it loads instructions, not executable code; later tool calls are
131
+ checked separately.
132
+ 4. Configured classifiers run in array order. A static stage may resolve low-risk
133
+ work; a decision stage uses OpenCode's native decision API; an LLM stage uses
134
+ its explicit reviewer. The first decisive allow or deny stops the chain.
135
+ 5. Exhaustion, unusable context, interruption, failure, or timeout resolves to
136
+ **ask**. A deny ends with `Continue with a safer alternative.`
137
+
138
+ Mode off restores the original host effect. A paused breaker turns a reviewed
139
+ native allow into `ask`; an existing native ask keeps its prompt.
140
+
141
+ Assuming classifiers: [static, decision, llm] (array order is authoritative).
142
+
143
+ ```mermaid
144
+ flowchart TD
145
+ A[permission evaluate<br/>allow / ask / deny] --> B{native deny?}
146
+ B -- yes --> B1[final, untouched<br/>audit only]
147
+ B -- no --> C{config valid &<br/>session on?}
148
+ C -- invalid --> C1[passthrough<br/>review disabled]
149
+ C -- off --> C2[restore native<br/>effect + message]
150
+ C -- paused --> C3[allow → ask<br/>ask → passthrough]
151
+
152
+ C -- on --> D[context fetch<br/>size ≤ 64 KiB]
153
+ D --> E[Code Mode hash identity<br/>execute:sha256]
154
+ E --> S
155
+
156
+ subgraph S [scoreRequest — every hand-written safety check lives here]
157
+ S1[shell: guardShell + visibleShellDeny<br/>parse / operand roles / recursive delete]
158
+ S2[tiers: protected paths, credential names,<br/>symlink traversal, project scope]
159
+ S3[read/edit/write: URL, scope, safeTarget]
160
+ S4[webfetch/websearch/execute/unknown → HIGH]
161
+ S5[git history → evaluateVcsGate]
162
+ S1 --> S2 --> S3 --> S4 --> S5
163
+ end
164
+
165
+ S -- deny --> X1[DENY]
166
+ S -- ask --> X2[ASK]
167
+ S -- low / medium / high --> R
168
+
169
+ subgraph R [runClassifiers]
170
+ R0{score deny/ask?} -- yes --> X1
171
+ R0 -- no --> R1[static: low → ALLOW<br/>else abstain]
172
+ R1 -- abstain --> R2[decision: native generate.decisions<br/>confidence ≥ 0.9]
173
+ R2 -- ask / low confidence --> R3[llm: native generate.text]
174
+ R2 -- allow / deny --> T[terminal]
175
+ R3 -- allow / deny --> T
176
+ R3 -- ask / unresolved --> X2
177
+ end
178
+
179
+ X1 --> F[modes.finish<br/>counts + breaker]
180
+ X2 --> F
181
+ T --> F
182
+ F --> G[apply effect to host]
326
183
  ```
327
184
 
328
- `build:gui` writes `dist/gui-experimental.cjs`, the file the `./gui-experimental`
329
- export points at. It runs automatically before publishing.
185
+ ## Safety limits
186
+
187
+ - **Native Auto/YOLO is client-local.** If enabled, OpenCode may auto-approve an
188
+ `ask` without a dialog, including a plugin ask. Plugin checks still run and
189
+ plugin denies still block, but the plugin cannot read, change, or synchronize
190
+ that switch. Disable client Auto/YOLO if you need prompts.
191
+ - **Invalid configuration acts disabled.** The plugin performs no review and
192
+ native behavior applies. That is passthrough, not fail-closed.
193
+ - Coverage is limited to the permission `evaluate` hook and the Code Mode
194
+ `execute.before` bridge. Native path grants are not an OS sandbox.
195
+ - Code Mode stays HIGH: JavaScript can hide egress behind aliases and computed
196
+ URLs. Its permission identity is the SHA-256 of the exact UTF-8 source. Old
197
+ raw-code approvals generally do not match.
198
+ - Other project plugins can affect residual effects; only run code you trust.
199
+
200
+ ## Audit
201
+
202
+ Audit is the only plugin-generated telemetry and is **off by default**. It is
203
+ local and unsafe: raw records are unredacted and can contain secrets, file
204
+ contents, prompts, and model outputs. If enabled, the fixed path is
205
+ `$XDG_STATE_HOME/opencode-auto-mode/decisions.jsonl` (or
206
+ `~/.local/state/opencode-auto-mode/decisions.jsonl` when `XDG_STATE_HOME` is not
207
+ absolute). Rotated archives are retained; delete them manually. See
208
+ [README.llm.md](README.llm.md#audit-and-replay) before enabling it.
209
+
210
+ ## More documentation
211
+
212
+ - [README.llm.md](README.llm.md) — installation instructions for an AI agent,
213
+ full options, migration from 1.0.x, audit, and troubleshooting.
214
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — repository map, local development,
215
+ tests, and release checks. It ships with the package.
330
216
 
331
217
  ## Credit
332
218
 
333
219
  Created originally by [Andrew Mellen (mellena1)](https://github.com/mellena1) in
334
- [mellena1/opencode-auto-mode](https://github.com/mellena1/opencode-auto-mode),
335
- also credited as the author in [package.json](package.json). This independent
336
- project retains the original [MIT license](LICENSE).
220
+ [mellena1/opencode-auto-mode](https://github.com/mellena1/opencode-auto-mode).
221
+ This independent project retains the original [MIT license](LICENSE).
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t=require("solid-js/web"),c=require("solid-js"),u=require("@opencode/plugin/gui"),C=require("@opencode/plugin/rpc"),q={type:"object",properties:{enabled:{type:"boolean"},configError:{type:"string"},classifiers:{type:"array",items:{type:"string"}},reviewTimeoutMs:{type:"number"},reviewing:{type:"array",items:{type:"object",properties:{sessionID:{type:"string"},reviewID:{type:"string"},startedAt:{type:"number"}},required:["sessionID","reviewID","startedAt"],additionalProperties:!1}},recent:{type:"array",items:{type:"object",properties:{reviewID:{type:"string"},sessionID:{type:"string"},action:{type:"string"},decision:{type:"string"},stage:{type:"string"},reason:{type:"string"},durationMs:{type:"number"},at:{type:"number"},errorKind:{type:"string"}},required:["reviewID","sessionID","action","decision","stage","reason","durationMs","at"],additionalProperties:!1}}},required:["enabled","classifiers","reviewTimeoutMs","reviewing","recent"],additionalProperties:!1},$={type:"object",properties:{reviewID:{type:"string"},sessionID:{type:"string"},action:{type:"string"},decision:{type:"string"},stage:{type:"string"},reason:{type:"string"},durationMs:{type:"number"},at:{type:"number"},errorKind:{type:"string"}},required:["reviewID","sessionID","action","decision","stage","reason","durationMs","at"],additionalProperties:!1},j=C.Rpc.define({id:"opencode-auto-mode",methods:{status:{input:{type:"object",properties:{sessionID:{type:"string"}},additionalProperties:!1},output:q},decisions:{input:{type:"object",properties:{sessionID:{type:"string"},limit:{type:"number"}},additionalProperties:!1},output:{type:"array",items:$}}},events:{decision:{schema:$}}});function K(e){return e===void 0?void 0:{key:e.key,sessionID:e.id,ownServer:e.server.builtin}}function T(){let e,i=0,l=0,s=0;return{select(o){return o===e?!1:(e=o,l=i,s=0,!0)},begin(o){if(o===e)return++i},accept(o){return o<=l||o<=s?!1:(s=o,!0)}}}function k(e,i){const l=T();return async s=>{if(l.select(s?.key)&&i(void 0),s===void 0||!s.ownServer)return;const o=l.begin(s.key);if(o!==void 0)try{const v=await e(s.sessionID);l.accept(o)&&i(v)}catch{}}}function y(e){return e?.recent.at(-1)}function A(e){return e===void 0||e.reviewing.length===0&&e.recent.length===0?"empty":"entries"}function L(e){if(e===void 0)return;if(e.reviewing.length>0)return"auto-mode reviewing…";const i=y(e);if(i!==void 0)return i.errorKind!==void 0?`auto-mode ${i.decision}: ${i.errorKind}`:`auto-mode ${i.decision}`}function O(e,i,l){if(e===void 0||e.reviewing.length>0)return!1;const s=y(e);return s!==void 0&&i-s.at>l}var E=t.template('<div style="padding:8px 12px;font-size:12px;display:flex;flex-direction:column;gap:6px">'),_=t.template("<div>"),N=t.template("<div> "),F=t.template("<div style=display:flex;flex-direction:column;gap:2px><span style=font-weight:600> · </span><span></span><span style=opacity:0.6> · <!>ms · ");const R=3e3,x=3e4,D={allow:"success",ask:"warning",deny:"danger"},z=u.Plugin.define({i18n:{en:{"command.open":"Open auto-mode decisions","panel.title":"Auto-mode","panel.empty":"No decisions in this session yet.","panel.disabled":"Auto-mode has no valid configuration; automatic review is disabled. Host permissions still apply.","panel.reviewing":"Reviewing…","pill.title":"Auto-mode decisions"}},setup(e){const[i,l]=c.createSignal(void 0),s=k(r=>e.client.rpc(j).status({sessionID:r}),l),o=()=>K(e.sessions.current()),[v,I]=c.createSignal(Date.now()),m=()=>{I(Date.now()),s(o())};u.createKeyed(()=>o()?.key,m,{otherwise:m});const S=setInterval(m,R);c.onCleanup(()=>clearInterval(S));const M={id:"decisions",get title(){return e.t("panel.title")},closable:"compact",fallback:!0};e.add(u.Panel,{id:"decisions",region:"side",mobile:{get title(){return e.t("panel.title")},order:60,kind:"menu",icon:"shield"},list:r=>r.open.includes("decisions")?[M]:[],render:()=>(()=>{var r=E();return t.insert(r,(()=>{var p=t.memo(()=>i()?.enabled===!1);return()=>p()?(()=>{var n=_();return t.insert(n,()=>e.t("panel.disabled")),n})():null})(),null),t.insert(r,(()=>{var p=t.memo(()=>A(i())==="empty");return()=>p()?(()=>{var n=_();return t.insert(n,()=>e.t("panel.empty")),n})():[t.createComponent(c.For,{get each(){return[...i()?.reviewing??[]]},children:n=>(()=>{var a=N(),d=a.firstChild;return t.insert(a,()=>e.t("panel.reviewing"),d),t.insert(a,()=>n.reviewID,null),a})()}),t.createComponent(c.For,{get each(){return[...i()?.recent??[]].reverse()},children:n=>(()=>{var a=F(),d=a.firstChild,f=d.firstChild,w=d.nextSibling,g=w.nextSibling,b=g.firstChild,h=b.nextSibling;return h.nextSibling,t.insert(d,()=>n.decision,f),t.insert(d,(()=>{var P=t.memo(()=>n.errorKind===void 0);return()=>P()?"":` (${n.errorKind})`})(),f),t.insert(d,()=>n.action,null),t.insert(w,()=>n.reason),t.insert(g,()=>n.stage,b),t.insert(g,()=>Math.round(n.durationMs),h),t.insert(g,()=>new Date(n.at).toLocaleTimeString(),null),a})()})]})(),null),r})()}),e.add(u.Command,()=>({id:"open",title:e.t("command.open"),group:"tools",run:()=>{const r=e.sessions.current();r&&e.layout.toggle(`${e.id}:decisions`,r)}})),e.add(u.ComposerPill,{id:"status",order:20,show:()=>{const r=i(),p=L(r);if(p===void 0||O(r,v(),x))return;const n=(r?.reviewing.length??0)>0;return{label:p,tone:n?"warning":D[y(r)?.decision??"allow"],title:e.t("pill.title"),rows:(r?.recent??[]).slice(-5).reverse().map(a=>({id:a.reviewID,label:`${a.decision} · ${a.action}`,tone:D[a.decision],detail:a.reason}))}}})}});exports.default=z;
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js"),t=require("@opencode/plugin/gui"),n=require("@opencode/plugin/rpc");var r={type:`object`,properties:{enabled:{type:`boolean`},mode:{type:`string`,enum:[`on`,`off`,`paused`,`invalid`]},configError:{type:`string`,maxLength:200},errorKind:{type:`string`,enum:[`config`,`storage`,`status`]},reviewTimeoutMs:{type:`number`},reviewing:{type:`array`,maxItems:256,items:{type:`object`,properties:{sessionID:{type:`string`},reviewID:{type:`string`},startedAt:{type:`number`}},required:[`sessionID`,`reviewID`,`startedAt`],additionalProperties:!1}},counts:{type:`object`,properties:{allow:{type:`integer`,minimum:0},ask:{type:`integer`,minimum:0},deny:{type:`integer`,minimum:0}},required:[`allow`,`ask`,`deny`],additionalProperties:!1},breaker:{type:`boolean`}},required:[`enabled`,`mode`,`reviewTimeoutMs`,`reviewing`,`counts`],additionalProperties:!1},i=n.Rpc.define({id:`opencode-auto-mode`,methods:{status:{input:{type:`object`,properties:{sessionID:{type:`string`,minLength:1,maxLength:256}},additionalProperties:!1},output:r},setMode:{input:{type:`object`,properties:{sessionID:{type:`string`,minLength:1,maxLength:256},mode:{type:`string`,enum:[`on`,`off`,`resume`]}},required:[`sessionID`,`mode`],additionalProperties:!1},output:r}},events:{decision:{schema:{type:`object`,properties:{reviewID:{type:`string`},sessionID:{type:`string`},action:{type:`string`,maxLength:128},decision:{type:`string`,maxLength:64},stage:{type:`string`,maxLength:64},reason:{type:`string`,maxLength:200},durationMs:{type:`number`},at:{type:`number`},errorKind:{type:`string`,maxLength:64}},required:[`reviewID`,`sessionID`,`action`,`decision`,`stage`,`reason`,`durationMs`,`at`],additionalProperties:!1}}}});function a(e,t){return JSON.stringify([e,t.directory,t.workspaceID])}function o(e){if(!(e?.location===void 0||!e.id||e.id.length>256||!e.location.directory))return{key:a(e.key,e.location),sessionID:e.id,ownServer:e.server.builtin,location:{...e.location}}}var s={enabled:!1,mode:`invalid`,configError:`status unavailable`,errorKind:`status`,reviewTimeoutMs:0,reviewing:[],counts:{allow:0,ask:0,deny:0}},c=5e3;function l(e,t,n){let r=new AbortController,i=new Promise((e,n)=>{r.signal.addEventListener(`abort`,()=>n(Error(t)),{once:!0})}),a=setTimeout(()=>r.abort(),e),o=Promise.resolve().then(()=>{if(r.signal.aborted)throw Error(t);return n(r.signal)});return{abort:()=>r.abort(),done:Promise.race([o,i]).finally(()=>clearTimeout(a))}}function u(e,t,n=c){let r,i,a=0,o=!1,u=()=>{i?.request.abort(),i=void 0};return Object.assign(c=>{if(o||(r!==c?.key&&(r=c?.key,a++,u(),t(void 0)),c===void 0||!c.ownServer))return Promise.resolve();if(i!==void 0)return i.done;let d=a,f=l(n,`status request aborted`,t=>e(c,t)),p=f.done.then(e=>{!o&&d===a&&t(e??{...s},c)},()=>{!o&&d===a&&t({...s},c)}).finally(()=>{i?.request===f&&(i=void 0)});return i={request:f,done:p},p},{invalidate(){a++,u()},dispose(){o=!0,a++,u()}})}function d(e,t,n,r=()=>{},i=c){let a=new Map,o=!1,s=Number.isFinite(i)&&i>0?i:c;return{pending:e=>a.has(e.key),run(i,c){if(o||i===void 0||!i.ownServer||c===void 0)return Promise.resolve();let u=a.get(i.key);if(u)return u.done;let d=l(s,`mode change aborted`,t=>e(i,c,t)),f=d.done.then(e=>{!o&&e!==void 0&&t(i,e)},()=>{o||n()}).finally(()=>{a.get(i.key)?.request===d&&a.delete(i.key),o||r()});return a.set(i.key,{request:d,done:f}),r(),f},dispose(){o=!0;for(let e of a.values())e.request.abort();a.clear()}}}function f(e){switch(e?.mode){case`on`:return e.enabled?`off`:`on`;case`off`:return`on`;case`paused`:return`resume`;default:return}}function p(e){if(e!==void 0)switch(e.mode){case`invalid`:return e.errorKind===`status`||e.errorKind===void 0&&e.configError===`status unavailable`?`status unavailable`:e.errorKind===`config`?`Invalid config — review disabled`:`Review unavailable`;case`paused`:return`paused (breaker)`;case`off`:return`off`;case`on`:return e.enabled?`auto`:`off`}}function m(e){if(e?.mode===`invalid`&&e.configError!==void 0&&e.configError!==`status unavailable`)return`${p(e)}: ${e.configError}`}function h(e){if(e!==void 0)return e.mode===`invalid`?`danger`:e.mode===`paused`?`warning`:e.mode===`on`&&e.enabled?`success`:void 0}function g(e,t,n=Date.now){let r=new Set,i=0,a=0;return o=>{let s=e();if(s===void 0||!s.ownServer||o.location.directory!==s.location.directory||o.location.workspaceID!==s.location.workspaceID||o.data===null||typeof o.data!=`object`)return;let c=o.data;if(c.sessionID!==s.sessionID||typeof c.reviewID!=`string`||c.reviewID.length>256)return;let l=c.stage===`breaker`?`breaker`:c.decision===`deny`?`deny`:void 0;if(l===void 0)return;let u=JSON.stringify([s.key,c.reviewID,l]);if(r.has(u))return;r.add(u),r.size>128&&r.delete(r.values().next().value);let d=n();d-i>=5e3&&(i=d,a=0),!(l===`deny`&&a>=4)&&(a++,t({kind:l,message:l===`breaker`?`Auto mode paused (breaker)`:`Auto mode denied`}))}}function _(e){let t,n,r,i=!1,a=()=>{n?.abort(),r?.(),n=void 0,r=void 0};return{update(o,s){if(i||o===t)return;a(),t=o;let c=new AbortController;n=c;try{r=s(t=>{c.signal.aborted||e(t)},c.signal)}catch{a(),t=void 0}},dispose(){i=!0,a(),t=void 0}}}var v=1e3,y=t.Plugin.define({setup(n){let[r,a]=(0,e.createSignal)(void 0),[s,c]=(0,e.createSignal)(0),l=()=>o(n.sessions.current()),y=u((e,t)=>n.client.rpc(i).status({sessionID:e.sessionID},{location:e.location,signal:t}),(e,t)=>{e===void 0?a(void 0):t!==void 0&&l()?.key===t.key&&a({key:t.key,value:e})}),b=d((e,t,r)=>(y.invalidate(),n.client.rpc(i).setMode({sessionID:e.sessionID,mode:t},{location:e.location,signal:r})),(e,t)=>{l()?.key===e.key&&a({key:e.key,value:t})},()=>n.toast.show({title:`Auto-mode`,description:`Could not change auto mode. Try again.`,variant:`error`,duration:5e3}),()=>c(e=>e+1)),x=_(g(l,e=>{n.toast.show({title:`Auto-mode`,description:e.message,variant:e.kind===`deny`?`error`:`default`,duration:5e3})})),S=()=>{let e=n.client;x.update(e,(t,n)=>e.rpc(i).events.on(`decision`,t,{signal:n}));let t=l();t!==void 0&&b.pending(t)||y(t)};(0,t.createKeyed)(()=>l()?.key,S,{otherwise:S});let C=setInterval(S,v);(0,e.onCleanup)(()=>{clearInterval(C),b.dispose(),y.dispose(),x.dispose()}),n.add(t.ComposerPill,{id:`status`,order:20,show:e=>{s();let t=o(e.session),n=r(),i=t?.ownServer&&n?.key===t.key?n.value:void 0,a=f(i),c=t!==void 0&&b.pending(t),l=c?`changing auto mode…`:p(i)??`status unavailable`,u=m(i);return{label:l,tone:h(i),title:c?`Auto-mode: change pending`:u===void 0?a===void 0?`Auto-mode unavailable`:`Auto-mode: ${a}`:`Auto-mode: ${u}`,run:!c&&a!==void 0&&t?.ownServer?()=>{b.run(t,a)}:void 0}}})}});exports.default=y;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mortz.prk/opencode-auto-mode",
3
- "version": "1.0.2",
3
+ "version": "1.1.2",
4
4
  "description": "Configurable automatic permission review for OpenCode v2.",
5
5
  "author": {
6
6
  "name": "Andrew Mellen",
@@ -18,7 +18,10 @@
18
18
  "license": "MIT",
19
19
  "files": [
20
20
  "src",
21
- "dist"
21
+ "dist",
22
+ "README.md",
23
+ "README.llm.md",
24
+ "CONTRIBUTING.md"
22
25
  ],
23
26
  "exports": {
24
27
  ".": {
@@ -36,7 +39,8 @@
36
39
  "./gui-experimental": "./dist/gui-experimental.cjs"
37
40
  },
38
41
  "scripts": {
39
- "lint": "eslint . --max-warnings=0",
42
+ "lint": "eslint . --max-warnings=0 && node scripts/complexity-lint.js",
43
+ "lint:complexity": "node scripts/complexity-lint.js",
40
44
  "typecheck": "tsc --noEmit",
41
45
  "test": "bun test",
42
46
  "test:integration": "bun run integration/opencode.ts",
@@ -44,10 +48,8 @@
44
48
  "prepublishOnly": "bun run build:gui"
45
49
  },
46
50
  "dependencies": {
47
- "@opencode/ai": "2.0.30",
48
51
  "@opencode/plugin": "2.2.0",
49
52
  "@opencode/schema": "2.2.0",
50
- "effect": "4.0.1",
51
53
  "shell-quote": "1.12.0"
52
54
  },
53
55
  "peerDependencies": {
@@ -59,16 +61,15 @@
59
61
  "@eslint/js": "^10.0.1",
60
62
  "@opencode/theme": "2.2.0",
61
63
  "@opentui/core": "0.7.0",
62
- "@opentui/keymap": "^0.6.6",
63
64
  "@opentui/solid": "0.7.0",
64
65
  "@types/node": "^26.6.4",
65
66
  "@types/shell-quote": "1.7.5",
66
67
  "@typescript/native": "npm:typescript@^7.0.2",
67
68
  "eslint": "^10.12.0",
68
- "solid-js": "1.9.12",
69
+ "solid-js": "1.9.17",
69
70
  "typescript": "npm:@typescript/typescript6@^6.0.2",
70
71
  "typescript-eslint": "^8.71.1",
71
- "vite": "^7.0.0",
72
+ "vite": "^8.3.5",
72
73
  "vite-plugin-solid": "^2.11.0"
73
74
  }
74
75
  }