opencode-plugin-flow 3.2.0 → 3.2.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/CHANGELOG.md +22 -0
- package/README.md +2 -2
- package/dist/cli.js +9 -9
- package/dist/index.js +29 -63
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.2.2] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
Critical fix: plugin failed to load on current OpenCode hosts (unbound SDK log method)
|
|
8
|
+
|
|
9
|
+
Every plugin load since the host SDK moved to generated client classes crashed at init with `Cannot read properties of undefined (reading '_client')`: plugin startup detached `app.log` from the host client (`const log = ctx.client?.app?.log`), and the SDK's `app.log` is a class method that reads `this._client`. OpenCode caught the throw, reported "failed to load plugin", and dropped Flow entirely — no tools, no config injection, no skill sync. This is the real cause behind `/flow-auto` reporting that `flow_status` was unavailable. The log call also passed the entry as top-level options where the SDK expects it under `body`; both are fixed — logging now goes through a bound, fail-silent wrapper posting `{ body: { service: "opencode-plugin-flow", level, message } }`.
|
|
10
|
+
|
|
11
|
+
The bug was invisible to every gate because all of them faked `app.log` as a plain function. The install smoke, bundle sanity, and the dist-load test now drive plugin init through an SDK-shaped fake client (a class whose `log` prototype method reads `this._client` and expects the `body` payload) and assert the startup log arrives through it.
|
|
12
|
+
|
|
13
|
+
Also fixed: bundle sanity and the dist-load test invoked the real plugin factory without sandboxing `$HOME`, so every `bun run check` silently synced dev-versioned skills, commands, and agents into the developer's real `~/.config/opencode` (the `version=0.0.0` markers some installs ended up with). Both now run against a temp home like the install smoke always did.
|
|
14
|
+
|
|
15
|
+
Upgrade by bumping the pin and restarting twice; the synced surface then re-syncs with real version markers.
|
|
16
|
+
|
|
17
|
+
## [3.2.1] - 2026-06-13
|
|
18
|
+
|
|
19
|
+
A missing Flow backend is now a stop condition in the skills, not a silent downgrade
|
|
20
|
+
|
|
21
|
+
A real `/flow-auto` run in an environment where the plugin had not loaded showed the gap: the slash commands and skills are markdown files OpenCode discovers from disk, so they keep working even when the `flow_*` tools are absent — and with no instruction for that case, the model improvised an ad-hoc unrecorded review that looked like success while recording no session, plan, evidence, or decision.
|
|
22
|
+
|
|
23
|
+
All four skills now treat missing `flow_*` tools as a stop-and-tell-the-user condition: the `flow` driving loop gets it as the first entry in its stop list (including the hint that a project-local `plugin` array overrides the global one), and `flow-plan`, `flow-run`, and `flow-review` each get a matching line at the point where they would first need their tool — each can be entered directly through its command without the driving loop. The instruction is explicit that substituting an unrecorded workflow is never acceptable.
|
|
24
|
+
|
|
25
|
+
Skill content only; no plugin code changes. Bump the pin and restart twice so the re-synced skills are picked up.
|
|
26
|
+
|
|
5
27
|
## [3.2.0] - 2026-06-13
|
|
6
28
|
|
|
7
29
|
Stale installs become visible: a passive update notice, the running version in `/flow-status`, and documented update steps
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/openco
|
|
|
29
29
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
32
|
-
"plugin": ["opencode-plugin-flow@3.2.
|
|
32
|
+
"plugin": ["opencode-plugin-flow@3.2.2"]
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -56,7 +56,7 @@ OpenCode never auto-updates plugins: the cached install for a given spec string
|
|
|
56
56
|
|
|
57
57
|
To update with an exact pin (recommended):
|
|
58
58
|
|
|
59
|
-
1. Change the pin in `opencode.json` to the new version
|
|
59
|
+
1. Change the pin in `opencode.json` to the new version (same form as the install snippet above).
|
|
60
60
|
2. Restart OpenCode once to install the new version, and a second time so the freshly re-synced skills, commands, and agents are discovered.
|
|
61
61
|
|
|
62
62
|
If you pinned a range like `@3` instead, the spec string never changes, so the cache entry must be cleared by hand before restarting:
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{homedir as ue}from"node:os";import{readdir as ae,readFile as
|
|
2
|
+
import{homedir as ue}from"node:os";import{readdir as ae,readFile as oe,rm as u,rmdir as se}from"node:fs/promises";import{dirname as ie,join as d,normalize as ne,sep as re}from"node:path";var $={fast:"low",balanced:"medium",deep:"high"},j={"flow-reviewer":{mode:"all",description:"Review Flow work read-only and record a reviewer decision.",prompt:"You are the Flow reviewer. Load the `flow-review` skill, review the requested work read-only, then record your decision with flow_review_record.",reasoningEffort:$.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},O={"flow-plan":{description:"Create, update, or approve a Flow plan",template:"Load the `flow-plan` skill and plan: $ARGUMENTS"},"flow-run":{description:"Run one approved Flow feature",template:"Load the `flow-run` skill and execute the next approved Flow feature. $ARGUMENTS"},"flow-auto":{description:"Drive the Flow loop autonomously until completion or a real blocker",template:"Load the `flow` skill and drive the Flow loop (status, plan, run, review) until completion or a real blocker: $ARGUMENTS"},"flow-status":{description:"Inspect the active Flow session and workspace readiness",template:"Call flow_status (detailed) and report session state, readiness checks, and the suggested next step."},"flow-review":{description:"Run a read-only Flow review with a fresh context",agent:"flow-reviewer",subtask:!0,template:"Load the `flow-review` skill and review: $ARGUMENTS"}},_=["flow-doctor","flow-history","flow-reset","flow-session"];import{createHash as K}from"node:crypto";import{join as w}from"node:path";var k=w(".config","opencode","skills"),m=w(".config","opencode","commands"),x=w(".config","opencode","agents"),v=".flow-skill-version",E="SKILL.md.backup",A=w(".config","opencode","plugins","flow.js"),R=`// Managed by flow-opencode install/uninstall
|
|
3
3
|
`,B="flow-opencode-generated-skill",M=`<!-- ${B} `,C=new RegExp(`^<!-- ${B} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function h(e){return K("sha256").update(e,"utf8").digest("hex")}function y(e){let t=e.split(`
|
|
4
|
-
`),
|
|
5
|
-
`);if(h(l)!==c)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:c}}}var z="opencode-plugin-flow",N="file=",
|
|
6
|
-
`)){if(!
|
|
7
|
-
`)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let
|
|
8
|
-
`)){let n=
|
|
4
|
+
`),o=t.flatMap((p,Z)=>p.startsWith(M)?[Z]:[]);if(o.length===0)return{kind:"not_generated"};if(o.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=o[0];if(a===void 0)return{kind:"not_generated"};let s=t[a];if(s===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let i=s.match(C);if(!i)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,c]=i;if(n===void 0||r===void 0||c===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let l=[...t.slice(0,a),...t.slice(a+1)].join(`
|
|
5
|
+
`);if(h(l)!==c)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:c}}}var z="opencode-plugin-flow",N="file=",W="=sha256:";function g(e){let t=new Map;for(let o of e.split(`
|
|
6
|
+
`)){if(!o.startsWith(N))continue;let a=o.slice(N.length),s=a.lastIndexOf(W);if(s===-1)continue;let i=a.slice(0,s),n=a.slice(s+W.length);if(i.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(i,n)}return t}function b(e,t,o){let a=new Map;for(let n of e.split(`
|
|
7
|
+
`)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let s=a.get("version"),i=a.get("hash");if(a.get("plugin")!==z||a.get("kind")!==t||a.get("name")!==o||!s||!i?.startsWith("sha256:"))return null;return{kind:t,name:o,version:s,hash:i.slice(7)}}function T(e){let t=new Map;for(let i of e.split(`
|
|
8
|
+
`)){let n=i.indexOf("=");if(n===-1)continue;t.set(i.slice(0,n),i.slice(n+1))}let o=t.get("plugin"),a=t.get("version");if(o!==z||!a)return null;let s=t.get("hash");return{plugin:o,version:a,hash:s?.startsWith("sha256:")?s.slice(7):null}}import{mkdir as Ye,readFile as D,rm as F,writeFile as He}from"node:fs/promises";import{dirname as Qe,join as V,sep as Xe}from"node:path";function P(e){return`${["---",`description: ${JSON.stringify(e.description)}`,...e.agent?[`agent: ${JSON.stringify(e.agent)}`]:[],...e.subtask===void 0?[]:[`subtask: ${e.subtask}`],"---"].join(`
|
|
9
9
|
`)}
|
|
10
10
|
|
|
11
11
|
${e.template}
|
|
@@ -13,7 +13,7 @@ ${e.template}
|
|
|
13
13
|
`)}
|
|
14
14
|
|
|
15
15
|
${e.prompt}
|
|
16
|
-
`}function G(){return new Map(Object.entries(
|
|
16
|
+
`}function G(){return new Map(Object.entries(O).map(([e,t])=>[e,P(t)]))}function Y(){return new Map(Object.entries(j).map(([e,t])=>[e,ee(t)]))}async function H(e){let t=[],o=[];for(let a of e.names){let s=V(e.root,`${a}.md`),i=V(e.root,`.${a}.flow-version`),n=await L(i),r=n===null?null:b(n,e.kind,a);if(r===null)continue;let c=await L(s);if(c!==null&&h(c)!==r.hash){o.push(s);continue}if(!e.dryRun)await F(s,{force:!0}),await F(i,{force:!0}),await F(`${s}.backup`,{force:!0});t.push(s)}return{removed:t,keptUserEdited:o}}async function L(e){try{return await D(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}function te(e){if(!e)return[];let t=["permission:"];for(let[o,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(o)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(o)}:`);for(let[s,i]of Object.entries(a))t.push(` ${JSON.stringify(s)}: ${JSON.stringify(i)}`)}}return t}async function Q({homeDir:e,dryRun:t=!1,logger:o}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},s=d(e,k);for(let c of await le(s)){if(c!=="flow"&&!c.startsWith("flow-"))continue;let l=d(s,c),p=await ce(l);if(p==="foreign")continue;if(p==="user_edited"){a.keptUserEditedSkills.push(l),o?.(`Kept user-edited Flow skill at ${l}; remove it manually if it is no longer needed.`);continue}if(!t)await de(l);a.removedSkills.push(l),o?.(`${t?"Would remove":"Removed"} Flow skill at ${l}.`)}let i=await H({kind:"command",root:d(e,m),names:_,dryRun:t});for(let c of i.removed)a.removedCommands.push(c),o?.(`${t?"Would remove":"Removed"} retired Flow command at ${c}.`);for(let c of i.keptUserEdited)a.keptUserEditedCommands.push(c),o?.(`Kept user-edited Flow command at ${c}; remove it manually if it is no longer needed.`);await U({homeDir:e,dryRun:t,logger:o,kind:"command",root:d(e,m),files:G(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await U({homeDir:e,dryRun:t,logger:o,kind:"agent",root:d(e,x),files:Y(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let n=d(e,A),r=await f(n);if(r!==null)if(r.startsWith(R)){if(!t)await u(n,{force:!0});a.removedPreNpmPlugin=n,o?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else a.keptForeignPreNpmPlugin=n,o?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return o?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),a}async function ce(e){let t=d(e,"SKILL.md"),o=await f(t),a=await f(d(e,v)),s=a===null?null:T(a);if(s!==null&&a!==null){for(let[r,c]of g(a)){if(r==="SKILL.md")continue;let l=X(e,r);if(l===null)continue;let p=await f(l);if(p!==null&&h(p)!==c)return"user_edited"}if(o===null)return"pristine";if(s.hash!==null&&h(o)===s.hash)return"pristine";return y(o).kind==="valid_generated"?"pristine":"user_edited"}if(o===null)return"foreign";let i=y(o);if(i.kind==="valid_generated")return"pristine";if(i.kind==="invalid_generated")return"user_edited";return"foreign"}function X(e,t){let o=ne(d(e,...t.split("/")));if(o!==e&&o.startsWith(`${e}${re}`))return o;return null}async function de(e){let t=await f(d(e,v)),o=new Set;if(t!==null)for(let a of g(t).keys()){let s=X(e,a);if(s===null)continue;await u(s,{force:!0}),await u(`${s}.backup`,{force:!0});let i=ie(s);if(i!==e)o.add(i)}await u(d(e,"SKILL.md"),{force:!0}),await u(d(e,v),{force:!0}),await u(d(e,E),{force:!0});for(let a of[...o].sort((s,i)=>i.length-s.length))await I(a);await I(e)}async function I(e){try{await se(e)}catch(t){let o=t.code;if(o!=="ENOENT"&&o!=="ENOTEMPTY")throw t}}async function le(e){try{return(await ae(e,{withFileTypes:!0})).filter((o)=>o.isDirectory()).map((o)=>o.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await oe(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function U(e){for(let[t,o]of e.files){let a=d(e.root,`${t}.md`),s=d(e.root,`.${t}.flow-version`),i=await f(a),n=await f(s),r=n===null?null:b(n,e.kind,t);if(i===null&&r===null)continue;if(!(r!==null||i===o))continue;if(i!==null&&r!==null&&h(i)!==r.hash){e.keptUserEdited.push(a),e.logger?.(`Kept user-edited Flow ${e.kind} at ${a}; remove it manually if it is no longer needed.`);continue}if(!e.dryRun)await u(a,{force:!0}),await u(s,{force:!0}),await u(`${a}.backup`,{force:!0});e.removed.push(a),e.logger?.(`${e.dryRun?"Would remove":"Removed"} Flow ${e.kind} at ${a}.`)}if(!e.dryRun)await I(e.root)}var q=`opencode-plugin-flow — Flow plugin lifecycle commands
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
19
|
bunx opencode-plugin-flow uninstall [--dry-run]
|
|
@@ -28,8 +28,8 @@ Options:
|
|
|
28
28
|
--dry-run Show what would be removed without deleting anything
|
|
29
29
|
--help Show this message`;function J(e){process.stdout.write(`${e}
|
|
30
30
|
`)}function S(e){process.stderr.write(`${e}
|
|
31
|
-
`)}async function pe(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return J(q),0;let
|
|
31
|
+
`)}async function pe(e){let t=[...e];if(t.length===0||t.includes("--help")||t.includes("-h"))return J(q),0;let o=t.shift();if(o!=="uninstall")return S(`Unknown command: ${o}
|
|
32
32
|
|
|
33
|
-
${q}`),1;let a=!1;for(let
|
|
33
|
+
${q}`),1;let a=!1;for(let s of t){if(s==="--dry-run"){a=!0;continue}return S(`Unknown argument: ${s}
|
|
34
34
|
|
|
35
35
|
${q}`),1}return await Q({homeDir:process.env.HOME??ue(),dryRun:a,logger:J}),0}try{process.exitCode=await pe(process.argv.slice(2))}catch(e){S(e instanceof Error?e.message:String(e)),process.exitCode=1}
|