@rockysoft/opencode-hybrid-workflow 0.0.0-stage → 0.3.2-beta.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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -3
  3. package/THIRD_PARTY_NOTICES.md +18 -0
  4. package/assets/agents/backend-engineer.md +167 -0
  5. package/assets/agents/code-reviewer.md +129 -0
  6. package/assets/agents/database-engineer.md +162 -0
  7. package/assets/agents/devops-engineer.md +115 -0
  8. package/assets/agents/frontend-engineer.md +38 -0
  9. package/assets/agents/fullstack-architect.md +43 -0
  10. package/assets/agents/security-auditor.md +132 -0
  11. package/assets/agents/technical-writer.md +78 -0
  12. package/assets/agents/test-engineer.md +154 -0
  13. package/assets/rules.md +93 -0
  14. package/assets/skills/frontend-design/INSTALLATION.json +9 -0
  15. package/assets/skills/frontend-design/LICENSE.txt +177 -0
  16. package/assets/skills/frontend-design/SKILL.md +78 -0
  17. package/assets/skills/ui-ux-pro-max/INSTALLATION.json +10 -0
  18. package/assets/skills/ui-ux-pro-max/LICENSE +21 -0
  19. package/assets/skills/ui-ux-pro-max/SKILL.md +221 -0
  20. package/assets/skills/ui-ux-pro-max/data/app-interface.csv +33 -0
  21. package/assets/skills/ui-ux-pro-max/data/catalog-summary.json +78 -0
  22. package/assets/skills/ui-ux-pro-max/data/charts.csv +26 -0
  23. package/assets/skills/ui-ux-pro-max/data/colors.csv +193 -0
  24. package/assets/skills/ui-ux-pro-max/data/data-provenance.json +1348 -0
  25. package/assets/skills/ui-ux-pro-max/data/google-font-licenses.json +20185 -0
  26. package/assets/skills/ui-ux-pro-max/data/google-fonts.csv +1935 -0
  27. package/assets/skills/ui-ux-pro-max/data/icons.csv +106 -0
  28. package/assets/skills/ui-ux-pro-max/data/landing.csv +35 -0
  29. package/assets/skills/ui-ux-pro-max/data/motion.csv +18 -0
  30. package/assets/skills/ui-ux-pro-max/data/phosphor-icons-upstream.json +32582 -0
  31. package/assets/skills/ui-ux-pro-max/data/products.csv +193 -0
  32. package/assets/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  33. package/assets/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  34. package/assets/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  35. package/assets/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
  36. package/assets/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  37. package/assets/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +60 -0
  38. package/assets/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
  39. package/assets/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  40. package/assets/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  41. package/assets/skills/ui-ux-pro-max/data/stacks/nextjs.csv +62 -0
  42. package/assets/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +77 -0
  43. package/assets/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +68 -0
  44. package/assets/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  45. package/assets/skills/ui-ux-pro-max/data/stacks/react.csv +67 -0
  46. package/assets/skills/ui-ux-pro-max/data/stacks/shadcn.csv +69 -0
  47. package/assets/skills/ui-ux-pro-max/data/stacks/svelte.csv +56 -0
  48. package/assets/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  49. package/assets/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  50. package/assets/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
  51. package/assets/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
  52. package/assets/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  53. package/assets/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
  54. package/assets/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
  55. package/assets/skills/ui-ux-pro-max/data/styles.csv +89 -0
  56. package/assets/skills/ui-ux-pro-max/data/typography.csv +75 -0
  57. package/assets/skills/ui-ux-pro-max/data/ui-reasoning.csv +193 -0
  58. package/assets/skills/ui-ux-pro-max/data/ux-guidelines.csv +120 -0
  59. package/assets/skills/ui-ux-pro-max/references/pro-rules.md +117 -0
  60. package/assets/skills/ui-ux-pro-max/references/quick-reference.md +256 -0
  61. package/assets/skills/ui-ux-pro-max/scripts/core.py +993 -0
  62. package/assets/skills/ui-ux-pro-max/scripts/design_system.py +1668 -0
  63. package/assets/skills/ui-ux-pro-max/scripts/reasoning_contract.py +123 -0
  64. package/assets/skills/ui-ux-pro-max/scripts/search.py +177 -0
  65. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json +36 -0
  66. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-catalog.json +33 -0
  67. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv +3 -0
  68. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json +17 -0
  69. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json +7 -0
  70. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv +4 -0
  71. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json +23 -0
  72. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json +13 -0
  73. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json +4 -0
  74. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json +5 -0
  75. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-baseline.json +3366 -0
  76. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-cases.json +111 -0
  77. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-thresholds.json +100 -0
  78. package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_refresh.py +354 -0
  79. package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_summary_line_endings.py +78 -0
  80. package/assets/skills/ui-ux-pro-max/scripts/tests/test_core.py +343 -0
  81. package/assets/skills/ui-ux-pro-max/scripts/tests/test_core_data_quality.py +198 -0
  82. package/assets/skills/ui-ux-pro-max/scripts/tests/test_data_contracts.py +421 -0
  83. package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +233 -0
  84. package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_stack.py +61 -0
  85. package/assets/skills/ui-ux-pro-max/scripts/tests/test_native_desktop_stack_freshness.py +179 -0
  86. package/assets/skills/ui-ux-pro-max/scripts/tests/test_relevance_evaluator.py +182 -0
  87. package/assets/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py +82 -0
  88. package/assets/skills/ui-ux-pro-max/scripts/tests/test_style_taxonomy.py +175 -0
  89. package/assets/skills/ui-ux-pro-max/scripts/tests/test_text_layout_resilience.py +120 -0
  90. package/assets/skills/ui-ux-pro-max/scripts/tests/test_web_stack_freshness.py +177 -0
  91. package/assets/skills/ui-ux-pro-max/scripts/validate_data.py +1095 -0
  92. package/assets/skills/web-design-guidelines/INSTALLATION.json +1 -0
  93. package/assets/skills/web-design-guidelines/SKILL.md +18 -0
  94. package/cli.mjs +91 -0
  95. package/core/controller.mjs +367 -0
  96. package/core/dashboard-state.js +48 -0
  97. package/core/dashboard.css +2 -0
  98. package/core/dashboard.html +12 -0
  99. package/core/dashboard.js +72 -0
  100. package/core/decision.mjs +22 -0
  101. package/core/design.mjs +139 -0
  102. package/core/evidence.mjs +82 -0
  103. package/core/frontend-policy.mjs +34 -0
  104. package/core/images.mjs +35 -0
  105. package/core/monitor-data.mjs +58 -0
  106. package/core/monitor.mjs +24 -0
  107. package/core/operation-lock.mjs +82 -0
  108. package/core/plugin.mjs +91 -0
  109. package/core/posix-lock.mjs +31 -0
  110. package/core/posix-lock.py +23 -0
  111. package/core/pricing.mjs +53 -0
  112. package/core/process-identity.mjs +19 -0
  113. package/core/research-tools.mjs +37 -0
  114. package/core/review-context.mjs +43 -0
  115. package/core/snapshot.mjs +95 -0
  116. package/core/telemetry.mjs +107 -0
  117. package/core/workers.mjs +192 -0
  118. package/core/workflow-policy.mjs +10 -0
  119. package/docs/configuration.md +134 -0
  120. package/docs/images/01-install.png +0 -0
  121. package/docs/images/02-reviewers.png +0 -0
  122. package/docs/images/03-connection.png +0 -0
  123. package/docs/images/04-provider.png +0 -0
  124. package/docs/images/05-model.png +0 -0
  125. package/docs/images/06-effort.png +0 -0
  126. package/docs/images/07-design-lead.png +0 -0
  127. package/docs/images/08-save.png +0 -0
  128. package/docs/images/09-settings.png +0 -0
  129. package/docs/images/10-browser.png +0 -0
  130. package/docs/images/11-browser-confirm.png +0 -0
  131. package/docs/images/12-browser-ready.png +0 -0
  132. package/docs/images/13-tools.png +0 -0
  133. package/docs/images/14-diagnostics.png +0 -0
  134. package/docs/images/15-connect-provider.png +0 -0
  135. package/docs/images/16-reopen.png +0 -0
  136. package/docs/images/17-implementation.png +0 -0
  137. package/docs/images/18-cli.png +0 -0
  138. package/docs/images/19-cli-model.png +0 -0
  139. package/docs/images/20-cli-effort.png +0 -0
  140. package/index.js +19 -0
  141. package/lib/commands.mjs +48 -0
  142. package/lib/config.mjs +63 -0
  143. package/lib/install.mjs +111 -0
  144. package/lib/native-browser.mjs +79 -0
  145. package/lib/opencode-reviewer.mjs +129 -0
  146. package/lib/platform.mjs +14 -0
  147. package/lib/reviewer-browser-proxy.mjs +61 -0
  148. package/lib/reviewer-browser.mjs +35 -0
  149. package/lib/reviewers.mjs +11 -0
  150. package/lib/select-reviewers.mjs +58 -0
  151. package/lib/settings-rpc.mjs +9 -0
  152. package/lib/settings-service.mjs +129 -0
  153. package/package.json +64 -6
  154. package/sidebar/reader.mjs +80 -0
  155. package/sidebar/settings.mjs +99 -0
  156. package/tui.js +303 -0
@@ -0,0 +1,129 @@
1
+ import {existsSync,readFileSync,writeFileSync,mkdirSync,renameSync} from 'node:fs';
2
+ import path from 'node:path';
3
+ import {createHash,randomUUID} from 'node:crypto';
4
+ import {configDirectory,readSettings,settingsFile,packageRoot} from './config.mjs';
5
+ import {install,parseJsonc,safeTarget} from './install.mjs';
6
+ import {selection} from './select-reviewers.mjs';
7
+ import {commandSpec} from './commands.mjs';
8
+ import {runProcess,requireSubscription} from '../core/workers.mjs';
9
+ import {NativeBrowser} from './native-browser.mjs';
10
+ import {SettingsRPC} from './settings-rpc.mjs';
11
+ const writes=new Set();
12
+ const unwrap=x=>x?.data??x;
13
+ export function revision(file){return createHash('sha256').update(existsSync(file)?readFileSync(file):'missing').digest('hex');}
14
+ export function validateChoices(input,catalog,previous){
15
+ if(!input.reviewers||Object.keys(input.reviewers).some(k=>!['astra','opus'].includes(k)))throw Error('Choose one or two reviewer slots.');
16
+ const reviewers={};
17
+ for(const who of ['astra','opus']){
18
+ const item=input.reviewers[who];
19
+ if(item===null&&who==='opus'){reviewers[who]=null;continue;}
20
+ if(!item||typeof item.model!=='string')throw Error('Reviewer model is required.');
21
+ if(item.adapter==='opencode')reviewers[who]=selection(`opencode:${item.providerID}/${item.model}`,{catalog,variant:item.variant});
22
+ else if(['codex-cli','claude-cli'].includes(item.adapter)){
23
+ if(item.effort&&!/^[a-zA-Z0-9_-]+$/.test(item.effort))throw Error('Invalid reasoning effort.');
24
+ reviewers[who]=selection(`${item.adapter}:${item.model}`,{previous:previous.reviewers?.[who]??{},variant:item.effort});
25
+ }else throw Error('Unknown reviewer connection.');
26
+ }
27
+ if(!['astra','opus'].includes(input.designAuthor)||!reviewers[input.designAuthor])throw Error('Choose an enabled planning/design lead.');
28
+ return {reviewers,designAuthor:input.designAuthor};
29
+ }
30
+ export function createSettingsService(ctx,options={}){
31
+ const root=configDirectory(),file=settingsFile(options);
32
+ const readOnly=path.resolve(file)!==path.join(root,'hybrid','config.json');
33
+ const load=()=>readSettings(options);
34
+ const catalog=async()=>{const values=unwrap(await ctx.model.list({location:ctx.location}));return values.filter(m=>m.enabled&&m.capabilities?.tools&&(!m.type||m.type==='chat'));};
35
+ const globalFile=()=>{const json=path.join(root,'opencode.json'),jsonc=path.join(root,'opencode.jsonc');if(existsSync(json)&&existsSync(jsonc))throw Error('Both opencode.json and opencode.jsonc exist; resolve the duplicate configuration.');return existsSync(jsonc)?jsonc:json;};
36
+ const globalRead=()=>existsSync(globalFile())?parseJsonc(readFileSync(safeTarget(root,globalFile()),'utf8')):{};
37
+ const patchGlobal=update=>{
38
+ const target=globalFile(),before=existsSync(target)?readFileSync(safeTarget(root,target),'utf8'):null;
39
+ const value=before?parseJsonc(before):{};update(value);
40
+ const backup=path.join(root,'backups','hybrid-ui-'+randomUUID());mkdirSync(backup,{recursive:true,mode:0o700});
41
+ if(before!==null)writeFileSync(path.join(backup,path.basename(target)),before,{mode:0o600});
42
+ const tmp=target+'.hybrid-'+randomUUID();writeFileSync(tmp,JSON.stringify(value,null,2)+'\n',{mode:0o600});renameSync(tmp,target);
43
+ };
44
+ const persist=patch=>install({root,settingsPatch:{...(!existsSync(file)?{setupComplete:false}:{}),...patch},registerPlugin:false});
45
+ async function exclusive(fn){if(readOnly)throw Error('This installation uses a custom OPENCODE_HYBRID_CONFIG override. It remains active; interactive changes require the standard global configuration location.');if(writes.has(file))throw Error('Another settings change is in progress. Please retry when it finishes.');writes.add(file);try{return await fn();}finally{writes.delete(file);}}
46
+ async function get(){
47
+ const config=load(),models=await catalog();
48
+ return {configured:existsSync(file)&&config.setupComplete!==false,readOnly,revision:revision(file),version:JSON.parse(readFileSync(path.join(packageRoot,'package.json'),'utf8')).version,
49
+ reviewers:config.reviewers,designAuthor:config.designAuthor??(config.reviewers.opus?'opus':'astra'),browserEnabled:!!config.reviewBrowser?.enabled,
50
+ catalog:models.map(m=>({providerID:m.providerID,id:m.id,name:m.name,variants:(m.variants??[]).map(v=>v.id),vision:m.capabilities.input?.includes('image')===true})),
51
+ configFile:file,implementationModel:globalRead().model??'Selected in OpenCode',host:process.platform};
52
+ }
53
+ async function diagnostics(){
54
+ const config=load(),checks=[];
55
+ const check=async(name,fn)=>{try{checks.push({name,ok:true,detail:await fn()});}catch(e){checks.push({name,ok:false,detail:e.message});}};
56
+ for(const [name,exe] of [['Node','node'],['Git','git'],['pnpm',config.pnpmExecutable],['Design research Python',config.design.pythonExecutable]])await check(name,async()=>{const spec=commandSpec(exe);return (await runProcess(spec.executable,[...spec.args,'--version'],root,'',15000)).trim().split('\n')[0];});
57
+ const models=await catalog();
58
+ for(const [who,lane] of Object.entries(config.reviewers))if(lane)await check(who==='astra'?'Reviewer 1':'Reviewer 2',async()=>{
59
+ if(lane.adapter==='opencode'){selection(`opencode:${lane.providerID}/${lane.model}`,{catalog:models,variant:lane.variant});return `${lane.providerID} / ${lane.model} / ${lane.variant??'default'}`;}
60
+ requireSubscription(who,config,root);return `${lane.adapter} signed in`;
61
+ });
62
+ if(config.reviewBrowser?.enabled)await check('Reviewer browser',async()=>{
63
+ const folder=path.join(root,'hybrid','checks',randomUUID());safeTarget(root,folder);mkdirSync(folder,{recursive:true});
64
+ const browser=new NativeBrowser(config,folder);
65
+ try{await browser.start();const result=await browser.request('tools/call',{name:'browser_snapshot',arguments:{}});if(result.isError)throw Error((result.content??[]).filter(c=>c.type==='text').map(c=>c.text).join('\n')||'Browser check failed');return 'Isolated browser launched and responded';}finally{await browser.close();}
66
+ });
67
+ for(const name of ['frontend-design','ui-ux-pro-max','web-design-guidelines'])await check(name,async()=>{if(!existsSync(path.join(config.design.skillsDirectory,name,'SKILL.md')))throw Error('Bundled skill missing');return 'Available';});
68
+ return {checks,modelCalls:0};
69
+ }
70
+ async function installTool(spec,signal){
71
+ const folder=path.join(root,'hybrid','tools');safeTarget(root,folder);mkdirSync(folder,{recursive:true,mode:0o700});
72
+ await runProcess('npm',['install','--prefix',folder,'--ignore-scripts','--no-audit','--no-fund',spec],folder,'',0,signal);
73
+ return folder;
74
+ }
75
+ return {get,diagnostics,
76
+ save:input=>exclusive(async()=>{
77
+ if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
78
+ const patch=validateChoices(input,await catalog(),load());
79
+ // Recheck after catalog I/O; never overwrite another UI's saved defaults.
80
+ if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
81
+ const installed=persist({...patch,setupComplete:true});return {saved:true,preservedCustomFiles:installed.preservedCustomFiles,message:'Saved globally. New tasks use these reviewers; active tasks keep their assignments.'};
82
+ }),
83
+ browser:(input,context={})=>exclusive(async()=>{
84
+ if(input.action==='disable'){persist({reviewBrowser:{...load().reviewBrowser,enabled:false}});return {message:'Reviewer browser disabled for future calls.'};}
85
+ if(input.action==='enable'){
86
+ let setting=load().reviewBrowser;
87
+ if(!setting?.command){
88
+ const server=globalRead().mcp?.servers?.playwright;
89
+ if(server?.type!=='local'||!Array.isArray(server.command))throw Error('No installed browser found. Choose Install browser support in this menu.');
90
+ const stop=server.command.findIndex((x,i)=>i>0&&x.startsWith('--')),command=server.command.slice(0,stop<0?undefined:stop);
91
+ if(command.some(x=>/^(-y|install|add)$/.test(x)))throw Error('Browser uses on-demand downloads. Choose Install browser support for a managed installation.');
92
+ const pos=server.command.indexOf('--browser');setting={command,browser:pos>=0?server.command[pos+1]:'chrome',headless:true};
93
+ }
94
+ persist({reviewBrowser:{...setting,enabled:true}});return {message:'Browser support enabled.'};
95
+ }
96
+ if(input.action!=='install')throw Error('Unknown browser action.');
97
+ const folder=await installTool('@playwright/mcp@0.0.83',context.signal),node=commandSpec('node');
98
+ await runProcess(node.executable,[...node.args,path.join(folder,'node_modules','playwright','cli.js'),'install','firefox'],folder,'',0,context.signal);
99
+ const command=[node.executable,...node.args,path.join(folder,'node_modules','@playwright','mcp','cli.js')];
100
+ persist({reviewBrowser:{enabled:true,command,browser:'firefox',headless:true}});
101
+ patchGlobal(value=>{value.mcp??={};value.mcp.servers??={};value.mcp.servers.playwright??={type:'local',command:[...command,'--browser','firefox','--isolated','--headless'],enabled:true};});
102
+ return {message:'Playwright and Firefox installed. Reviewer browser enabled; implementation MCP configured unless one already existed.'};
103
+ }),
104
+ dependencies:(input,context={})=>exclusive(async()=>{
105
+ if(input.action==='pnpm'){
106
+ const folder=await installTool('pnpm@12.14.0',context.signal);
107
+ // pnpm 12 ships a native binary; only run this explicitly selected package's
108
+ // installer, then verify it before saving a working executable.
109
+ const node=commandSpec('node'),packageFolder=path.join(folder,'node_modules','pnpm');
110
+ await runProcess(node.executable,[...node.args,path.join(packageFolder,'install.js')],folder,'',0,context.signal);
111
+ const executable=path.join(packageFolder,process.platform==='win32'?'pnpm.exe':'pnpm');
112
+ await runProcess(executable,['--version'],folder,'',15000,context.signal);
113
+ persist({verification:{...load().verification,executable}});
114
+ return {message:'Managed pnpm installed for hybrid verification. Project-required versions still take precedence.'};
115
+ }
116
+ if(input.action==='context7'){
117
+ patchGlobal(value=>{value.mcp??={};value.mcp.servers??={};value.mcp.servers.context7??={type:'remote',url:'https://mcp.context7.com/mcp',enabled:true};});
118
+ return {message:'Context7 documentation MCP configured. Any existing configuration was preserved.'};
119
+ }
120
+ throw Error('Unknown dependency.');
121
+ }),
122
+ };
123
+ }
124
+ export async function registerSettings(ctx,options){
125
+ const service=createSettingsService(ctx,options);
126
+ // Expected setup errors must reach the dialog, not become opaque HTTP defects.
127
+ const handlers=Object.fromEntries(Object.entries(service).map(([name,fn])=>[name,async(...args)=>{try{return await fn(...args);}catch(error){return {error:{message:error.message}};}}]));
128
+ await ctx.rpc.register(SettingsRPC,handlers);return service;
129
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
- {
2
- "name": "@rockysoft/opencode-hybrid-workflow",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@rockysoft/opencode-hybrid-workflow",
3
+ "version": "0.3.2-beta.2",
4
+ "description": "Portable configurable-reviewer planning, design, implementation checkpoints and usage sidebar for OpenCode 2",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "RockySoft",
8
+ "keywords": ["opencode", "opencode-plugin", "agents", "code-review", "workflow"],
9
+ "publishConfig": { "access": "public", "tag": "beta" },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./tui": "./tui.js"
13
+ },
14
+ "bin": {
15
+ "opencode-hybrid": "./cli.mjs"
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "tui.js",
20
+ "cli.mjs",
21
+ "lib/",
22
+ "core/",
23
+ "sidebar/",
24
+ "assets/",
25
+ "README.md",
26
+ "THIRD_PARTY_NOTICES.md",
27
+ "LICENSE",
28
+ "docs/"
29
+ ],
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "scripts": {
34
+ "build": "node scripts/build.mjs",
35
+ "prepack": "npm run build",
36
+ "test": "node --test test/*.test.mjs",
37
+ "doctor": "node cli.mjs doctor"
38
+ },
39
+ "devDependencies": {
40
+ "@babel/core": "7.29.7",
41
+ "@babel/preset-typescript": "7.27.1",
42
+ "babel-preset-solid": "1.9.12"
43
+ },
44
+ "peerDependencies": {
45
+ "@opencode/plugin": ">=2.1.2 <3",
46
+ "@opentui/core": ">=0.6.6",
47
+ "@opentui/solid": ">=0.6.6",
48
+ "solid-js": ">=1.9.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@opencode/plugin": {
52
+ "optional": true
53
+ },
54
+ "@opentui/core": {
55
+ "optional": true
56
+ },
57
+ "@opentui/solid": {
58
+ "optional": true
59
+ },
60
+ "solid-js": {
61
+ "optional": true
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,80 @@
1
+ import { readFile, realpath, readdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { reportedUsage, priceCall } from '../core/pricing.mjs';
5
+ import { summarize, countedCalls } from '../core/dashboard-state.js';
6
+ import { runtimeFor } from '../lib/config.mjs';
7
+
8
+ export const projectKey = directory => createHash('sha256').update(process.platform==='win32'?directory.toLowerCase():directory).digest('hex').slice(0,24);
9
+ export const number = value => value == null ? 'Unavailable' : value.toLocaleString('en-US');
10
+ export function money(cost) {
11
+ if (!cost) return 'Unavailable';
12
+ const usd = n => '$' + n.toFixed(2);
13
+ return Math.abs(cost.high-cost.low) < .005 ? usd(cost.low) : usd(cost.low) + '–' + usd(cost.high);
14
+ }
15
+ export function cards(snapshot, reset) {
16
+ return summarize(countedCalls(snapshot.calls, reset), snapshot.efforts).filter(total=>snapshot.models?.[total.who]||snapshot.calls.some(c=>c.who===total.who)||!snapshot.models).map(total => {
17
+ const active = snapshot.calls.filter(c => c.who === total.who && !c.endedAt && ['running','preparing','stale'].includes(c.status));
18
+ const names=[...new Set(snapshot.calls.filter(c=>c.who===total.who).map(c=>c.requestedModel).filter(Boolean))];
19
+ const efforts=[...new Set(snapshot.calls.filter(c=>c.who===total.who).map(c=>c.effort).filter(Boolean))];
20
+ return { ...total,effort:efforts.length?efforts.join(' / '):total.effort, model:names.length?names.join(' / '):snapshot.models?.[total.who],status: active.some(c => c.status === 'stale') ? 'Status stale' : active.length ? 'Running' : 'Idle',
21
+ partial: total.measuredCalls < total.calls || total.pricedCalls < total.calls };
22
+ });
23
+ }
24
+
25
+ // Read only this project's existing telemetry, never prompts, credentials, or source.
26
+ // No HTTP server, child processes, model requests, or checkpoint writes.
27
+ export function createReader(settings) {
28
+ const cache = new Map();
29
+ async function json(file) {
30
+ const info = await stat(file);
31
+ const key = `${info.mtimeMs}:${info.size}`;
32
+ const old = cache.get(file);
33
+ if (old?.key === key) return old.value;
34
+ const value = JSON.parse(await readFile(file, 'utf8'));
35
+ cache.set(file, { key, value });
36
+ if (cache.size > 512) cache.delete(cache.keys().next().value);
37
+ return value;
38
+ }
39
+ async function contained(root, file) {
40
+ const resolved = await realpath(file);
41
+ const relative = path.relative(root, resolved);
42
+ if (relative === '..' || relative.startsWith('..'+path.sep) || path.isAbsolute(relative)) throw new Error('Telemetry path outside runtime');
43
+ return resolved;
44
+ }
45
+ return async directory => {
46
+ if (typeof directory !== 'string' || !path.isAbsolute(directory)) throw new Error('Project location unavailable');
47
+ const project = await realpath(directory);
48
+ const efforts = settings.efforts;
49
+ const empty = { project, task: null, key: projectKey(project), calls: [], efforts, models:settings.models };
50
+ let runtime, folder, state;
51
+ try {
52
+ runtime = await realpath(runtimeFor(settings,project));
53
+ folder = await contained(runtime, path.join(runtime, projectKey(project)));
54
+ state = await json(await contained(runtime, path.join(folder, 'state.json')));
55
+ } catch (error) { if (error.code === 'ENOENT') return empty; throw error; }
56
+ if (!/^[a-f0-9-]{36}$/i.test(state.id)) throw new Error('Invalid telemetry task');
57
+ const normalize=p=>process.platform==='win32'?p.toLowerCase():p;
58
+ if (!state.baseline?.root || normalize(await realpath(state.baseline.root)) !== normalize(project)) throw new Error('Project telemetry does not match current project');
59
+ const result = { ...empty, task: state.id, key: `${empty.key}:${state.id}` };
60
+ let taskFolder;
61
+ try { taskFolder = await contained(runtime, path.join(folder, state.id)); }
62
+ catch (error) { if (error.code === 'ENOENT') return result; throw error; }
63
+ const names = (await readdir(taskFolder)).filter(name => /^\d+-(astra|opus)-[a-z0-9-]+$/.test(name)).sort((a,b) => parseInt(a)-parseInt(b));
64
+ for (const name of names) {
65
+ const who = /^\d+-(astra|opus)-/.exec(name)[1];
66
+ let activity;
67
+ try { activity = await json(await contained(runtime, path.join(taskFolder, name, 'activity.json'))); }
68
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
69
+ let status = activity?.status ?? 'unknown';
70
+ if (['running','preparing'].includes(status) && !(Date.now()-Date.parse(activity.updatedAt) <= 30000)) status = 'stale';
71
+ const call = { id: name, who, status, endedAt: activity?.endedAt ?? null,
72
+ effort:activity?.effort,requestedModel: activity?.requestedModel ?? settings.models[who], resolvedModels: activity?.resolvedModels ?? [],
73
+ apiCost:activity?.apiCost,usage: activity?.usage ?? null, usageByModel: activity?.usageByModel ?? {} };
74
+ call.usage = reportedUsage(call);
75
+ call.cost = priceCall(call);
76
+ result.calls.push(call);
77
+ }
78
+ return result;
79
+ };
80
+ }
@@ -0,0 +1,99 @@
1
+ import {SettingsRPC} from '../lib/settings-rpc.mjs';
2
+ const unwrap=value=>value?.data??value;
3
+ const label=lane=>lane?`${lane.adapter==='opencode'?lane.providerID:lane.adapter} / ${lane.model} / ${lane.variant??lane.effort??'default'}`:'Disabled';
4
+ export function createSettingsUI(context,onSaved=()=>{}){
5
+ const rpc=context.client.rpc(SettingsRPC);let open=false,disposed=false;
6
+ const select=(title,options,current)=>context.ui.dialog.select({title,current,options});
7
+ const alert=(title,message)=>context.ui.dialog.alert({title,message});
8
+ async function invoke(method,input={}){const response=await rpc[method](input),value=unwrap(response);const error=response?.error??value?.error;if(error)throw Error(typeof error==='string'?error:error.message??'OpenCode could not complete this settings operation.');return value;}
9
+ function dispatch(name){
10
+ const commands=context.keymap.commands();
11
+ const command=commands.find(c=>c.slash?.name===name||c.slash?.aliases?.includes(name));
12
+ if(!command)throw Error(`OpenCode's ${name} menu is unavailable in this version.`);
13
+ context.keymap.dispatch(command.id);
14
+ }
15
+ async function reviewer(who,state,current){
16
+ const connection=await select(`Reviewer ${who==='astra'?1:2}: connection`,[{title:'OpenCode connected provider',value:'opencode',description:'Use an account already connected to OpenCode'},{title:'External CLI',value:'cli',description:'Codex CLI or Claude Code CLI login'}],!state.configured||current?.adapter==='opencode'?'opencode':'cli');
17
+ if(!connection)return;
18
+ if(connection==='opencode'){
19
+ const providers=[...new Set(state.catalog.map(m=>m.providerID))];
20
+ if(!providers.length){await alert('Connect a provider','No connected tool-capable models are available. Use Connect provider in Hybrid settings, then try again.');return;}
21
+ const provider=await select('Provider',providers.map(id=>({title:id,value:id})),current?.providerID);if(!provider)return;
22
+ const model=await select('Reviewer model',state.catalog.filter(m=>m.providerID===provider).map(m=>({title:m.name??m.id,value:m.id,description:m.id+(m.vision?' · images supported':' · text only')})),current?.model);if(!model)return;
23
+ const chosen=state.catalog.find(m=>m.providerID===provider&&m.id===model);
24
+ const variant=await select('Reasoning effort',[{title:'Provider default',value:'__default'},...chosen.variants.map(v=>({title:v,value:v}))],current?.variant??'__default');if(!variant)return;
25
+ return {adapter:'opencode',providerID:provider,model,...(variant==='__default'?{}:{variant})};
26
+ }
27
+ const adapter=await select('External CLI',[{title:'Codex CLI',value:'codex-cli'},{title:'Claude Code CLI',value:'claude-cli'}],current?.adapter);if(!adapter)return;
28
+ const model=await context.ui.dialog.prompt({title:'CLI model',placeholder:current?.adapter===adapter?current.model:adapter==='codex-cli'?'gpt-6-astra':'opus',value:current?.adapter===adapter?current.model:adapter==='codex-cli'?'gpt-6-astra':'opus'});if(!model?.trim())return;
29
+ const effort=await select('CLI reasoning effort',['low','medium','high','xhigh','max'].map(v=>({title:v,value:v})),current?.effort??'high');if(!effort)return;
30
+ return {adapter,model:model.trim(),effort};
31
+ }
32
+ async function wizard(state){
33
+ const count=await select('Hybrid setup: reviewers',[{title:'Two reviewers',value:2,description:'Independent planning and review'},{title:'One reviewer',value:1,description:'A single premium planner/reviewer'}],state.reviewers.opus?2:1);if(!count)return;
34
+ const first=await reviewer('astra',state,state.reviewers.astra);if(!first)return;
35
+ const second=count===2?await reviewer('opus',state,state.reviewers.opus):null;if(count===2&&!second)return;
36
+ const lead=count===1?'astra':await select('Planning and design lead',[{title:first.model,value:'astra',description:'Reviewer 1'},{title:second.model,value:'opus',description:'Reviewer 2'}],state.designAuthor);if(!lead)return;
37
+ const confirmed=await context.ui.dialog.confirm({title:'Save Hybrid setup',message:`Reviewer 1: ${label(first)}\nReviewer 2: ${label(second)}\nPlanning/design lead: ${lead==='astra'?first.model:second.model}\n\nSave for all projects. Active tasks retain their existing reviewers.`,label:{confirm:'Save',cancel:'Cancel'}});
38
+ if(!confirmed)return;
39
+ const result=await invoke('save',{revision:state.revision,reviewers:{astra:first,opus:second},designAuthor:lead});
40
+ onSaved();await alert('Hybrid setup saved',result.message+(result.preservedCustomFiles?.length?'\nExisting customized agents and skills were preserved.':''));
41
+ }
42
+ async function browser(state){
43
+ const action=await select('Reviewer browser',[
44
+ {title:state.browserEnabled?'Disable browser access':'Enable existing browser',value:state.browserEnabled?'disable':'enable'},
45
+ {title:'Install browser support',value:'install',description:'Download managed Playwright MCP and Firefox; enable reviewer access'},
46
+ ]);if(!action)return;
47
+ if(action==='install'&&!await context.ui.dialog.confirm({title:'Install browser support',message:'Download Playwright MCP and Firefox on the OpenCode server machine? Existing browser settings and user profiles will be preserved.',label:{confirm:'Install',cancel:'Cancel'}}))return;
48
+ context.ui.toast.show({message:action==='install'?'Installing browser support…':'Updating browser settings…',variant:'info'});
49
+ const result=await invoke('browser',{action});onSaved();await alert('Reviewer browser',result.message);
50
+ }
51
+ async function dependencies(){
52
+ const action=await select('Tools and dependencies',[
53
+ {title:'Install pnpm for verification',value:'pnpm',description:'Managed installation; does not replace your global pnpm'},
54
+ {title:'Enable Context7 documentation',value:'context7',description:'Configure official remote documentation MCP'},
55
+ {title:'System prerequisites',value:'system',description:'Node, Git, Python and optional CLI login guidance'},
56
+ ]);if(!action)return;
57
+ if(action==='system'){await alert('System prerequisites','OpenCode and Node 22+ are required. Git is needed for projects; Python 3 supports design research. External CLI choices also require that CLI and its official login. Diagnostics identifies what is missing. System installers and account sign-in remain machine/account setup; the plugin never copies credentials.');return;}
58
+ if(!await context.ui.dialog.confirm({title:'Set up dependency',message:action==='pnpm'?'Install managed pnpm on the OpenCode server?':'Add the official Context7 documentation MCP? Existing settings are preserved.',label:{confirm:'Set up',cancel:'Cancel'}}))return;
59
+ context.ui.toast.show({message:'Setting up dependency…',variant:'info'});const result=await invoke('dependencies',{action});await alert('Dependency setup',result.message);
60
+ }
61
+ async function show(firstRun=false){
62
+ if(open||disposed)return;open=true;
63
+ try{
64
+ let state=await invoke('get');
65
+ if(firstRun&&state.configured)return;
66
+ if(!state.configured){await wizard(state);state=await invoke('get');}
67
+ while(!disposed){
68
+ const action=await select(`Hybrid Workflow ${state.version}`, [
69
+ {title:state.configured?'Reviewers and design lead':'Complete setup',value:'reviewers',description:label(state.reviewers.astra)+' | '+label(state.reviewers.opus)},
70
+ {title:'Connect provider',value:'connect',description:'Open OpenCode’s account connection menu'},
71
+ {title:'Implementation model',value:'models',description:'Select the model that builds your code in OpenCode'},
72
+ {title:'Reviewer browser',value:'browser',description:state.browserEnabled?'Enabled · isolated sessions':'Disabled'},
73
+ {title:'Tools and dependencies',value:'dependencies',description:'Browser installation is in Reviewer browser'},
74
+ {title:'Run diagnostics',value:'diagnostics',description:'Check setup and logins without model calls'},
75
+ {title:'About this setup',value:'about',description:'Global configuration and active-task behavior'},
76
+ ]);
77
+ if(!action)return;
78
+ try{
79
+ if(action==='reviewers')await wizard(state);
80
+ else if(action==='connect'||action==='models'){dispatch(action);return;}
81
+ else if(action==='browser')await browser(state);
82
+ else if(action==='dependencies')await dependencies();
83
+ else if(action==='diagnostics'){
84
+ context.ui.toast.show({message:'Checking Hybrid setup…',variant:'info'});
85
+ const report=await invoke('diagnostics');
86
+ while(!disposed){
87
+ const item=await select('Hybrid diagnostics',report.checks.map((c,i)=>({title:`${c.ok?'OK':'Needs attention'} · ${c.name}`,value:String(i),description:c.detail.split('\n')[0]})));
88
+ if(item===undefined||item===null)break;
89
+ const check=report.checks[Number(item)];await alert(check.name,check.detail);
90
+ }
91
+ }else if(action==='about')await alert('Hybrid Workflow',`Global settings: ${state.configFile}\nServer platform: ${state.host}\nNew tasks use saved defaults. Active tasks preserve assigned reviewers, findings and history.\nAt least one pass and no unresolved medium/high/critical findings are required.\nSkills and agent rules are installed when setup is saved. No separate dashboard is needed.`);
92
+ }catch(error){await alert('Hybrid settings',error.message);}
93
+ state=await invoke('get');
94
+ }
95
+ }catch(error){context.ui.toast.show({message:'Hybrid settings: '+error.message,variant:'error'});}
96
+ finally{open=false;}
97
+ }
98
+ return {show,dispose(){disposed=true;},rpc};
99
+ }