opencode-plugin-flow 3.3.20 → 3.3.21

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 CHANGED
@@ -6,6 +6,10 @@ the rejected alternatives, and the validation that was run — see
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.3.21] - 2026-06-14
10
+
11
+ Nest `flow_review_record` payloads under `featureReview` or `finalReview` so the host schema is simple and the runtime reviewer schemas stay strict.
12
+
9
13
  ## [3.3.20] - 2026-06-14
10
14
 
11
15
  Hide the Flow reviewer behind `/flow-review` and `/flow-auto`, keeping direct reviewer selection advisory instead of a confusing session entry point.
package/README.md CHANGED
@@ -31,7 +31,7 @@ Add Flow to the `plugin` array in your `opencode.json` (global `~/.config/openco
31
31
 
32
32
  ```json
33
33
  {
34
- "plugin": ["opencode-plugin-flow@3.3.20"]
34
+ "plugin": ["opencode-plugin-flow@3.3.21"]
35
35
  }
36
36
  ```
37
37
 
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{homedir as ge}from"node:os";import{readdir as ce,readFile as de,rm as u,rmdir as le}from"node:fs/promises";import{dirname as ue,join as d,normalize as he,sep as pe}from"node:path";var M={fast:"low",balanced:"medium",deep:"high"},W={"flow-reviewer":{mode:"subagent",hidden:!0,description:"Internal read-only Flow review subagent. Use /flow-review or /flow-auto.",prompt:"You are the Flow reviewer, a hidden read-only subagent. Load `flow-review`; call flow_status first. If no active compatible Flow session exists, do advisory review only and do not call flow_review_record. After one successful/no-op flow_review_record, report the verdict and stop.",reasoningEffort:M.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},V={"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"}},R=["flow-doctor","flow-history","flow-reset","flow-session"];import{createHash as E}from"node:crypto";import{join as g}from"node:path";var A=g(".config","opencode","skills"),m=g(".config","opencode","commands"),I=g(".config","opencode","agents"),v=".flow-skill-version",G="SKILL.md.backup",T=g(".config","opencode","plugins","flow.js"),q=`// Managed by flow-opencode install/uninstall
2
+ import{homedir as ge}from"node:os";import{readdir as ce,readFile as le,rm as u,rmdir as de}from"node:fs/promises";import{dirname as ue,join as l,normalize as he,sep as pe}from"node:path";var M={fast:"low",balanced:"medium",deep:"high"},W={"flow-reviewer":{mode:"subagent",hidden:!0,description:"Internal read-only Flow review subagent. Use /flow-review or /flow-auto.",prompt:"You are the Flow reviewer, a hidden read-only subagent. Load `flow-review`; call flow_status first. If no active compatible Flow session exists, do advisory review only and do not call flow_review_record. After one successful/no-op flow_review_record, report the verdict and stop.",reasoningEffort:M.deep,permission:{edit:"deny",bash:"deny",task:{"*":"deny"},"flow_*":"deny",flow_status:"allow",flow_review_record:"allow"}}},V={"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"}},R=["flow-doctor","flow-history","flow-reset","flow-session"];import{createHash as E}from"node:crypto";import{join as g}from"node:path";var A=g(".config","opencode","skills"),m=g(".config","opencode","commands"),I=g(".config","opencode","agents"),v=".flow-skill-version",G="SKILL.md.backup",T=g(".config","opencode","plugins","flow.js"),q=`// Managed by flow-opencode install/uninstall
3
3
  `,X="flow-opencode-generated-skill",L=`<!-- ${X} `,ee=new RegExp(`^<!-- ${X} name=([a-z0-9]+(?:-[a-z0-9]+)*) version=([0-9]+) hash=sha256:([a-f0-9]{64}) -->$`,"u");function h(e){return E("sha256").update(e,"utf8").digest("hex")}function y(e){let t=e.split(`
4
- `),i=t.flatMap((p,K)=>p.startsWith(L)?[K]:[]);if(i.length===0)return{kind:"not_generated"};if(i.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=i[0];if(a===void 0)return{kind:"not_generated"};let o=t[a];if(o===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let s=o.match(ee);if(!s)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,c]=s;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 Q="opencode-plugin-flow",P="file=",C="=sha256:";function b(e){let t=new Map;for(let i of e.split(`
4
+ `),i=t.flatMap((p,K)=>p.startsWith(L)?[K]:[]);if(i.length===0)return{kind:"not_generated"};if(i.length>1)return{kind:"invalid_generated",reason:"duplicate_marker"};let a=i[0];if(a===void 0)return{kind:"not_generated"};let o=t[a];if(o===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let s=o.match(ee);if(!s)return{kind:"invalid_generated",reason:"malformed_marker"};let[,n,r,c]=s;if(n===void 0||r===void 0||c===void 0)return{kind:"invalid_generated",reason:"malformed_marker"};let d=[...t.slice(0,a),...t.slice(a+1)].join(`
5
+ `);if(h(d)!==c)return{kind:"invalid_generated",reason:"hash_mismatch"};return{kind:"valid_generated",marker:{name:n,version:r,hash:c}}}var Q="opencode-plugin-flow",P="file=",C="=sha256:";function b(e){let t=new Map;for(let i of e.split(`
6
6
  `)){if(!i.startsWith(P))continue;let a=i.slice(P.length),o=a.lastIndexOf(C);if(o===-1)continue;let s=a.slice(0,o),n=a.slice(o+C.length);if(s.length>0&&/^[a-f0-9]{64}$/.test(n))t.set(s,n)}return t}function k(e,t,i){let a=new Map;for(let n of e.split(`
7
7
  `)){let r=n.indexOf("=");if(r===-1)continue;a.set(n.slice(0,r),n.slice(r+1))}let o=a.get("version"),s=a.get("hash");if(a.get("plugin")!==Q||a.get("kind")!==t||a.get("name")!==i||!o||!s?.startsWith("sha256:"))return null;return{kind:t,name:i,version:o,hash:s.slice(7)}}function F(e){let t=new Map;for(let s of e.split(`
8
8
  `)){let n=s.indexOf("=");if(n===-1)continue;t.set(s.slice(0,n),s.slice(n+1))}let i=t.get("plugin"),a=t.get("version");if(i!==Q||!a)return null;let o=t.get("hash");return{plugin:i,version:a,hash:o?.startsWith("sha256:")?o.slice(7):null}}import{mkdir as Dt,rm as S,writeFile as Kt}from"node:fs/promises";import{join as Y,sep as Et}from"node:path";import{readFile as ae}from"node:fs/promises";async function w(e){try{return await ae(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}function J(e){return`${["---",`description: ${JSON.stringify(e.description)}`,...e.agent?[`agent: ${JSON.stringify(e.agent)}`]:[],...e.subtask===void 0?[]:[`subtask: ${e.subtask}`],"---"].join(`
@@ -13,7 +13,7 @@ ${e.template}
13
13
  `)}
14
14
 
15
15
  ${e.prompt}
16
- `}function x(){return new Map(Object.entries(V).map(([e,t])=>[e,J(t)]))}function _(){return new Map(Object.entries(W).map(([e,t])=>[e,Z(t)]))}async function j(e){let t=[],i=[];for(let a of e.names){let o=Y(e.root,`${a}.md`),s=Y(e.root,`.${a}.flow-version`),n=await w(s),r=n===null?null:k(n,e.kind,a);if(r===null)continue;let c=await w(o);if(c!==null&&h(c)!==r.hash){i.push(o);continue}if(!e.dryRun)await S(o,{force:!0}),await S(s,{force:!0}),await S(`${o}.backup`,{force:!0});t.push(o)}return{removed:t,keptUserEdited:i}}function ie(e){if(!e)return[];let t=["permission:"];for(let[i,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(i)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(i)}:`);for(let[o,s]of Object.entries(a))t.push(` ${JSON.stringify(o)}: ${JSON.stringify(s)}`)}}return t}async function O({homeDir:e,dryRun:t=!1,logger:i}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},o=d(e,A);for(let c of await we(o)){if(c!=="flow"&&!c.startsWith("flow-"))continue;let l=d(o,c),p=await fe(l);if(p==="foreign")continue;if(p==="user_edited"){a.keptUserEditedSkills.push(l),i?.(`Kept user-edited Flow skill at ${l}; remove it manually if it is no longer needed.`);continue}if(!t)await ve(l);a.removedSkills.push(l),i?.(`${t?"Would remove":"Removed"} Flow skill at ${l}.`)}let s=await j({kind:"command",root:d(e,m),names:R,dryRun:t});for(let c of s.removed)a.removedCommands.push(c),i?.(`${t?"Would remove":"Removed"} retired Flow command at ${c}.`);for(let c of s.keptUserEdited)a.keptUserEditedCommands.push(c),i?.(`Kept user-edited Flow command at ${c}; remove it manually if it is no longer needed.`);await $({homeDir:e,dryRun:t,logger:i,kind:"command",root:d(e,m),files:x(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await $({homeDir:e,dryRun:t,logger:i,kind:"agent",root:d(e,I),files:_(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let n=d(e,T),r=await f(n);if(r!==null)if(r.startsWith(q)){if(!t)await u(n,{force:!0});a.removedPreNpmPlugin=n,i?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else a.keptForeignPreNpmPlugin=n,i?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return i?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),a}async function fe(e){let t=d(e,"SKILL.md"),i=await f(t),a=await f(d(e,v)),o=a===null?null:F(a);if(o!==null&&a!==null){for(let[r,c]of b(a)){if(r==="SKILL.md")continue;let l=N(e,r);if(l===null)continue;let p=await f(l);if(p!==null&&h(p)!==c)return"user_edited"}if(i===null)return"pristine";if(o.hash!==null&&h(i)===o.hash)return"pristine";return y(i).kind==="valid_generated"?"pristine":"user_edited"}if(i===null)return"foreign";let s=y(i);if(s.kind==="valid_generated")return"pristine";if(s.kind==="invalid_generated")return"user_edited";return"foreign"}function N(e,t){let i=he(d(e,...t.split("/")));if(i!==e&&i.startsWith(`${e}${pe}`))return i;return null}async function ve(e){let t=await f(d(e,v)),i=new Set;if(t!==null)for(let a of b(t).keys()){let o=N(e,a);if(o===null)continue;await u(o,{force:!0}),await u(`${o}.backup`,{force:!0});let s=ue(o);if(s!==e)i.add(s)}await u(d(e,"SKILL.md"),{force:!0}),await u(d(e,v),{force:!0}),await u(d(e,G),{force:!0});for(let a of[...i].sort((o,s)=>s.length-o.length))await U(a);await U(e)}async function U(e){try{await le(e)}catch(t){let i=t.code;if(i!=="ENOENT"&&i!=="ENOTEMPTY")throw t}}async function we(e){try{return(await ce(e,{withFileTypes:!0})).filter((i)=>i.isDirectory()).map((i)=>i.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await de(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function $(e){for(let[t,i]of e.files){let a=d(e.root,`${t}.md`),o=d(e.root,`.${t}.flow-version`),s=await f(a),n=await f(o),r=n===null?null:k(n,e.kind,t);if(s===null&&r===null)continue;if(!(r!==null||s===i))continue;if(s!==null&&r!==null&&h(s)!==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(o,{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 U(e.root)}var B=`opencode-plugin-flow — Flow plugin lifecycle commands
16
+ `}function x(){return new Map(Object.entries(V).map(([e,t])=>[e,J(t)]))}function _(){return new Map(Object.entries(W).map(([e,t])=>[e,Z(t)]))}async function j(e){let t=[],i=[];for(let a of e.names){let o=Y(e.root,`${a}.md`),s=Y(e.root,`.${a}.flow-version`),n=await w(s),r=n===null?null:k(n,e.kind,a);if(r===null)continue;let c=await w(o);if(c!==null&&h(c)!==r.hash){i.push(o);continue}if(!e.dryRun)await S(o,{force:!0}),await S(s,{force:!0}),await S(`${o}.backup`,{force:!0});t.push(o)}return{removed:t,keptUserEdited:i}}function ie(e){if(!e)return[];let t=["permission:"];for(let[i,a]of Object.entries(e)){if(typeof a==="string"){t.push(` ${JSON.stringify(i)}: ${JSON.stringify(a)}`);continue}if(a&&typeof a==="object"){t.push(` ${JSON.stringify(i)}:`);for(let[o,s]of Object.entries(a))t.push(` ${JSON.stringify(o)}: ${JSON.stringify(s)}`)}}return t}async function O({homeDir:e,dryRun:t=!1,logger:i}){let a={removedSkills:[],keptUserEditedSkills:[],removedCommands:[],keptUserEditedCommands:[],removedAgents:[],keptUserEditedAgents:[],removedPreNpmPlugin:null,keptForeignPreNpmPlugin:null},o=l(e,A);for(let c of await we(o)){if(c!=="flow"&&!c.startsWith("flow-"))continue;let d=l(o,c),p=await fe(d);if(p==="foreign")continue;if(p==="user_edited"){a.keptUserEditedSkills.push(d),i?.(`Kept user-edited Flow skill at ${d}; remove it manually if it is no longer needed.`);continue}if(!t)await ve(d);a.removedSkills.push(d),i?.(`${t?"Would remove":"Removed"} Flow skill at ${d}.`)}let s=await j({kind:"command",root:l(e,m),names:R,dryRun:t});for(let c of s.removed)a.removedCommands.push(c),i?.(`${t?"Would remove":"Removed"} retired Flow command at ${c}.`);for(let c of s.keptUserEdited)a.keptUserEditedCommands.push(c),i?.(`Kept user-edited Flow command at ${c}; remove it manually if it is no longer needed.`);await $({homeDir:e,dryRun:t,logger:i,kind:"command",root:l(e,m),files:x(),removed:a.removedCommands,keptUserEdited:a.keptUserEditedCommands}),await $({homeDir:e,dryRun:t,logger:i,kind:"agent",root:l(e,I),files:_(),removed:a.removedAgents,keptUserEdited:a.keptUserEditedAgents});let n=l(e,T),r=await f(n);if(r!==null)if(r.startsWith(q)){if(!t)await u(n,{force:!0});a.removedPreNpmPlugin=n,i?.(`${t?"Would remove":"Removed"} pre-npm Flow plugin copy at ${n}.`)}else a.keptForeignPreNpmPlugin=n,i?.(`Kept ${n}: it is not managed by Flow. Remove it manually if it is a stale Flow copy.`);return i?.('Finally, remove "opencode-plugin-flow" from the plugin array in opencode.json and restart OpenCode.'),a}async function fe(e){let t=l(e,"SKILL.md"),i=await f(t),a=await f(l(e,v)),o=a===null?null:F(a);if(o!==null&&a!==null){for(let[r,c]of b(a)){if(r==="SKILL.md")continue;let d=N(e,r);if(d===null)continue;let p=await f(d);if(p!==null&&h(p)!==c)return"user_edited"}if(i===null)return"pristine";if(o.hash!==null&&h(i)===o.hash)return"pristine";return y(i).kind==="valid_generated"?"pristine":"user_edited"}if(i===null)return"foreign";let s=y(i);if(s.kind==="valid_generated")return"pristine";if(s.kind==="invalid_generated")return"user_edited";return"foreign"}function N(e,t){let i=he(l(e,...t.split("/")));if(i!==e&&i.startsWith(`${e}${pe}`))return i;return null}async function ve(e){let t=await f(l(e,v)),i=new Set;if(t!==null)for(let a of b(t).keys()){let o=N(e,a);if(o===null)continue;await u(o,{force:!0}),await u(`${o}.backup`,{force:!0});let s=ue(o);if(s!==e)i.add(s)}await u(l(e,"SKILL.md"),{force:!0}),await u(l(e,v),{force:!0}),await u(l(e,G),{force:!0});for(let a of[...i].sort((o,s)=>s.length-o.length))await U(a);await U(e)}async function U(e){try{await de(e)}catch(t){let i=t.code;if(i!=="ENOENT"&&i!=="ENOTEMPTY")throw t}}async function we(e){try{return(await ce(e,{withFileTypes:!0})).filter((i)=>i.isDirectory()).map((i)=>i.name).sort()}catch(t){if(t.code==="ENOENT")return[];throw t}}async function f(e){try{return await le(e,"utf8")}catch(t){if(t.code==="ENOENT")return null;throw t}}async function $(e){for(let[t,i]of e.files){let a=l(e.root,`${t}.md`),o=l(e.root,`.${t}.flow-version`),s=await f(a),n=await f(o),r=n===null?null:k(n,e.kind,t);if(s===null&&r===null)continue;if(!(r!==null||s===i))continue;if(s!==null&&r!==null&&h(s)!==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(o,{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 U(e.root)}var B=`opencode-plugin-flow — Flow plugin lifecycle commands
17
17
 
18
18
  Usage:
19
19
  bunx opencode-plugin-flow uninstall [--dry-run]