create-workframe 0.1.18 → 0.1.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.
Files changed (136) hide show
  1. package/README.md +1 -1
  2. package/bin/create-workframe.js +73 -9
  3. package/package.json +1 -1
  4. package/scripts/bundle-workframe-ui.mjs +11 -1
  5. package/workframe-api/action_proxy.py +8 -0
  6. package/workframe-api/chat_sessions.py +37 -6
  7. package/workframe-api/chat_stream.py +16 -0
  8. package/workframe-api/db_schema.py +29 -0
  9. package/workframe-api/handler_modules/handler_admin.py +32 -0
  10. package/workframe-api/handler_modules/handler_auth.py +6 -4
  11. package/workframe-api/handler_modules/handler_chat.py +17 -26
  12. package/workframe-api/handler_modules/handler_provider.py +13 -14
  13. package/workframe-api/handler_modules/handler_workspace.py +60 -2
  14. package/workframe-api/hermes_profiles.py +31 -9
  15. package/workframe-api/llm_proxy.py +18 -0
  16. package/workframe-api/mention_invoke.py +29 -24
  17. package/workframe-api/message_reactions.py +139 -0
  18. package/workframe-api/model_surface.py +329 -113
  19. package/workframe-api/package.json +4 -4
  20. package/workframe-api/profile_gateway.py +14 -6
  21. package/workframe-api/provider_bindings.py +118 -17
  22. package/workframe-api/provider_bootstrap.py +74 -38
  23. package/workframe-api/provider_model_catalog.py +261 -0
  24. package/workframe-api/rooms.py +4 -3
  25. package/workframe-api/route_registry.py +4 -0
  26. package/workframe-api/runtime_cohort.py +118 -23
  27. package/workframe-api/scripts/run-python.mjs +28 -0
  28. package/workframe-api/server.py +44 -12
  29. package/workframe-api/test_agent_identity_authority.py +96 -0
  30. package/workframe-api/test_chat_attribution_history.py +59 -0
  31. package/workframe-api/test_enrich_room_messages.py +43 -2
  32. package/workframe-api/test_llm_proxy_encoding.py +33 -0
  33. package/workframe-api/test_message_reactions.py +151 -0
  34. package/workframe-api/test_model_routing_reconcile.py +122 -0
  35. package/workframe-api/test_model_surface_consistency.py +45 -5
  36. package/workframe-api/test_oauth_llm.py +1 -1
  37. package/workframe-api/test_profile_gateway_health.py +37 -0
  38. package/workframe-api/test_provider_bindings.py +115 -1
  39. package/workframe-api/test_provider_model_catalog.py +27 -0
  40. package/workframe-api/test_provider_model_surface.py +2 -2
  41. package/workframe-api/test_runtime_lease_lifecycle.py +93 -0
  42. package/workframe-api/test_server_reexports.py +2 -0
  43. package/workframe-api/test_user_llm_prefs.py +27 -0
  44. package/workframe-api/test_user_theme_prefs.py +65 -0
  45. package/workframe-api/test_workspace_file_actions.py +119 -0
  46. package/workframe-api/turn_credentials.py +41 -0
  47. package/workframe-api/turn_overlay.py +95 -34
  48. package/workframe-api/user_prefs.py +22 -1
  49. package/workframe-api/workspace_bootstrap.py +1 -0
  50. package/workframe-api/workspace_files.py +124 -0
  51. package/workframe-api/zk_auth.py +7 -1
  52. package/workframe-ui/public/assets/{arc-CSddOtMa.js → arc-D_gXvZHy.js} +1 -1
  53. package/workframe-ui/public/assets/architecture-7EHR7CIX-B8ZI_Iqb.js +1 -0
  54. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BSiUei3D.js → architectureDiagram-3BPJPVTR-2F65XJkk.js} +1 -1
  55. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BhKTULPo.js → blockDiagram-GPEHLZMM-BoURuzr5.js} +1 -1
  56. package/workframe-ui/public/assets/{brave-B885JYnH.svg → brave-BoYMbZi1.svg} +0 -1
  57. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Bv_xZi-a.js → c4Diagram-AAUBKEIU-BBtveYSm.js} +1 -1
  58. package/workframe-ui/public/assets/channel-Ca1FvZ7w.js +1 -0
  59. package/workframe-ui/public/assets/{chunk-2J33WTMH-Bh0WiqFE.js → chunk-2J33WTMH-BCuhFg2u.js} +1 -1
  60. package/workframe-ui/public/assets/{chunk-3OPIFGDE-qN09pRuK.js → chunk-3OPIFGDE-gM6Q-rjJ.js} +1 -1
  61. package/workframe-ui/public/assets/{chunk-4BX2VUAB-Dz6FVuMa.js → chunk-4BX2VUAB-D11CJMfF.js} +1 -1
  62. package/workframe-ui/public/assets/{chunk-55IACEB6-DWtPHVfR.js → chunk-55IACEB6-tOvPBzSf.js} +1 -1
  63. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-CyOUeDAo.js → chunk-5ZQYHXKU-Bb1uF7QP.js} +1 -1
  64. package/workframe-ui/public/assets/{chunk-727SXJPM-BrICvWwx.js → chunk-727SXJPM-BvzzLkGB.js} +1 -1
  65. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CQu2SmLM.js → chunk-AQP2D5EJ-tr5A2PUD.js} +1 -1
  66. package/workframe-ui/public/assets/{chunk-BSJP7CBP-uJbYXNNG.js → chunk-BSJP7CBP-BSFEsidk.js} +1 -1
  67. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CCS_FtVZ.js → chunk-CSCIHK7Q-Bgf-IHHq.js} +1 -1
  68. package/workframe-ui/public/assets/{chunk-FMBD7UC4-CoLUtPiK.js → chunk-FMBD7UC4-rD27Gf3U.js} +1 -1
  69. package/workframe-ui/public/assets/{chunk-KSCS5N6A-DLW4gvEW.js → chunk-KSCS5N6A-C1MobXen.js} +1 -1
  70. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CMt0LJAd.js → chunk-L5ZTLDWV-CD2wS3bn.js} +1 -1
  71. package/workframe-ui/public/assets/{chunk-LZXEDZCA-Bn-maIlt.js → chunk-LZXEDZCA-DQxuSIdE.js} +2 -2
  72. package/workframe-ui/public/assets/{chunk-ND2GUHAM-CG7yoApR.js → chunk-ND2GUHAM-DHlBi932.js} +1 -1
  73. package/workframe-ui/public/assets/{chunk-NZK2D7GU-B4dIvbii.js → chunk-NZK2D7GU-MB8bO5o8.js} +1 -1
  74. package/workframe-ui/public/assets/{chunk-O5CBEL6O-B5spDyXy.js → chunk-O5CBEL6O-BmPwoKAr.js} +1 -1
  75. package/workframe-ui/public/assets/chunk-QZHKN3VN-BsuC0swk.js +1 -0
  76. package/workframe-ui/public/assets/chunk-WU5MYG2G-CyfRhD17.js +1 -0
  77. package/workframe-ui/public/assets/{chunk-XPW4576I-D5c0TMX6.js → chunk-XPW4576I-BUCJoags.js} +1 -1
  78. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DZ_KubnM.js +1 -0
  79. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DZ_KubnM.js +1 -0
  80. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DFRM3DZv.js → cose-bilkent-S5V4N54A-w9hpFlQU.js} +1 -1
  81. package/workframe-ui/public/assets/{dagre-BM42HDAG-CnUdKeJa.js → dagre-BM42HDAG-DEnBMVrR.js} +1 -1
  82. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ds3-e-hW.js → diagram-2AECGRRQ-CMoYqkVC.js} +1 -1
  83. package/workframe-ui/public/assets/{diagram-5GNKFQAL-B41zx8FF.js → diagram-5GNKFQAL-rS0QQe2R.js} +1 -1
  84. package/workframe-ui/public/assets/{diagram-KO2AKTUF-D83tI9I8.js → diagram-KO2AKTUF-Do63Vbzi.js} +1 -1
  85. package/workframe-ui/public/assets/{diagram-LMA3HP47-h_EuFyLI.js → diagram-LMA3HP47-Bq3e2FDx.js} +1 -1
  86. package/workframe-ui/public/assets/{diagram-OG6HWLK6-CpU-d3RZ.js → diagram-OG6HWLK6-DTESfGaG.js} +1 -1
  87. package/workframe-ui/public/assets/{dist-_igeSJ9r.js → dist-BiIfSARy.js} +1 -1
  88. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-jLFoncRU.js → erDiagram-TEJ5UH35-C7RA0P76.js} +1 -1
  89. package/workframe-ui/public/assets/eventmodeling-FCH6USID-DChy3wGd.js +1 -0
  90. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C9IaNgCy.js → flowDiagram-I6XJVG4X-e47AwJAm.js} +1 -1
  91. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-A9FhfFc5.js → ganttDiagram-6RSMTGT7-BVytj000.js} +1 -1
  92. package/workframe-ui/public/assets/{gemini-Cq-0-rmr.svg → gemini-DqnZlvMo.svg} +0 -1
  93. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BqK_IjZb.js → gitGraph-WXDBUCRP-QdT2gCGh.js} +1 -1
  94. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CDVqxhyW.js → gitGraphDiagram-PVQCEYII-COEavQVV.js} +1 -1
  95. package/workframe-ui/public/assets/index-03WsLxfn.js +133 -0
  96. package/workframe-ui/public/assets/index-BIKn2D6J.css +1 -0
  97. package/workframe-ui/public/assets/{info-J43DQDTF-C2EKRglL.js → info-J43DQDTF-CEyVZ-9w.js} +1 -1
  98. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-azQtFzpU.js → infoDiagram-5YYISTIA-CV3ftI3x.js} +1 -1
  99. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BuDOLv1t.js → ishikawaDiagram-YF4QCWOH-M8CoaInm.js} +1 -1
  100. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-ypTHFoT0.js → journeyDiagram-JHISSGLW-BtoaC0oj.js} +1 -1
  101. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Cgr78J_x.js → kanban-definition-UN3LZRKU-Dke-ozKB.js} +1 -1
  102. package/workframe-ui/public/assets/{line-CcwgaMdv.js → line-CDg_Ms_2.js} +1 -1
  103. package/workframe-ui/public/assets/{linear-CswIlWaW.js → linear-D3OWfPXS.js} +1 -1
  104. package/workframe-ui/public/assets/{mermaid-parser.core-CQYhP37X.js → mermaid-parser.core-CGDFs4AZ.js} +2 -2
  105. package/workframe-ui/public/assets/{mermaid.core-Cz1XNBaK.js → mermaid.core-C9-q_XHF.js} +3 -3
  106. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BJLu0829.js → mindmap-definition-RKZ34NQL-BvEiBAWz.js} +1 -1
  107. package/workframe-ui/public/assets/{packet-YPE3B663-AB1qh96V.js → packet-YPE3B663-FF5nKMbE.js} +1 -1
  108. package/workframe-ui/public/assets/{pie-LRSECV5Y-CokIXLdT.js → pie-LRSECV5Y-CZ9ijSae.js} +1 -1
  109. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyrwQn5F.js → pieDiagram-4H26LBE5-kD9o509O.js} +1 -1
  110. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-C_Fdg3F6.js → quadrantDiagram-W4KKPZXB-d_eA7_-T.js} +1 -1
  111. package/workframe-ui/public/assets/{radar-GUYGQ44K-C21Xfrrg.js → radar-GUYGQ44K-ViyzC1rg.js} +1 -1
  112. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ChhaWgaF.js → requirementDiagram-4Y6WPE33-Sq3D_b4Z.js} +1 -1
  113. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-C5T4sDTt.js → sankeyDiagram-5OEKKPKP-ZZnl3ZwZ.js} +1 -1
  114. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DRks8i3A.js → sequenceDiagram-3UESZ5HK-Yaz2MgMq.js} +1 -1
  115. package/workframe-ui/public/assets/{src-BQu5waHd.js → src-DBh8rT2Z.js} +1 -1
  116. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BI-AKYta.js → stateDiagram-AJRCARHV-DVwi_P3W.js} +1 -1
  117. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DRSxEx1_.js +1 -0
  118. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DnAIWzEc.js → timeline-definition-PNZ67QCA-C88RoMRK.js} +1 -1
  119. package/workframe-ui/public/assets/{treeView-BLDUP644-BEF_sjx7.js → treeView-BLDUP644-2xabSpow.js} +1 -1
  120. package/workframe-ui/public/assets/{treemap-LRROVOQU-BcbL7g5c.js → treemap-LRROVOQU-BA_oRW4e.js} +1 -1
  121. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DAoIbCkz.js → vennDiagram-CIIHVFJN-DvyVA588.js} +1 -1
  122. package/workframe-ui/public/assets/{wardley-L42UT6IY-3SOmFBFw.js → wardley-L42UT6IY-BR0suGqb.js} +1 -1
  123. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DlQV_-LH.js → wardleyDiagram-YWT4CUSO-CXMvBEO9.js} +1 -1
  124. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-CVoTTRHF.js → xychartDiagram-2RQKCTM6-wM7UrTHx.js} +1 -1
  125. package/workframe-ui/public/index.html +23 -12
  126. package/workframe-ui/public/workframe-build.json +4 -3
  127. package/workframe-ui/public/assets/architecture-7EHR7CIX-CDDNM1ni.js +0 -1
  128. package/workframe-ui/public/assets/channel-R52LrLO3.js +0 -1
  129. package/workframe-ui/public/assets/chunk-QZHKN3VN-C_9MbT10.js +0 -1
  130. package/workframe-ui/public/assets/chunk-WU5MYG2G-VJadzJmf.js +0 -1
  131. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CpBjaI3g.js +0 -1
  132. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CpBjaI3g.js +0 -1
  133. package/workframe-ui/public/assets/eventmodeling-FCH6USID-kzOxPvtj.js +0 -1
  134. package/workframe-ui/public/assets/index-Bco9IDMA.css +0 -1
  135. package/workframe-ui/public/assets/index-Do34PZS2.js +0 -132
  136. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-vJyhFZbx.js +0 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Published on npm as **create-workframe**.
4
4
 
5
5
  ```bash
6
- npx create-workframe@0.1.18 MyProject
6
+ npx create-workframe@0.1.21 MyProject
7
7
  ```
8
8
 
9
9
  Scaffolds an isolated Workframe + Hermes project on Windows, macOS, and Linux.
@@ -1360,10 +1360,27 @@ echo "Done. Mission control and workframe unchanged unless you recreate the full
1360
1360
  `;
1361
1361
  }
1362
1362
 
1363
- function launchPhaseBInstaller(target) {
1363
+ function launchPhaseBInstaller(target, { wait = false, noBrowser = false } = {}) {
1364
1364
  if (process.platform === 'win32') {
1365
1365
  const script = path.join(target, 'scripts', 'start-install.ps1');
1366
1366
  if (!fs.existsSync(script)) return false;
1367
+ if (wait) {
1368
+ const args = [
1369
+ '-NoProfile',
1370
+ '-ExecutionPolicy',
1371
+ 'Bypass',
1372
+ '-File',
1373
+ script,
1374
+ '-NoPrompt',
1375
+ ];
1376
+ if (noBrowser) args.push('-NoBrowser');
1377
+ const child = spawnSync('powershell.exe', args, { cwd: target, stdio: 'inherit' });
1378
+ if (child.error) throw child.error;
1379
+ if (child.status !== 0) {
1380
+ throw new Error(`Phase B installer failed (${child.status ?? 'unknown exit'})`);
1381
+ }
1382
+ return true;
1383
+ }
1367
1384
  const escaped = script.replace(/'/g, "''");
1368
1385
  const child = spawn(
1369
1386
  'powershell.exe',
@@ -1386,6 +1403,16 @@ function launchPhaseBInstaller(target) {
1386
1403
  }
1387
1404
  const script = path.join(target, 'scripts', 'start-install.sh');
1388
1405
  if (!fs.existsSync(script)) return false;
1406
+ if (wait) {
1407
+ const args = ['--no-prompt'];
1408
+ if (noBrowser) args.push('--no-browser');
1409
+ const child = spawnSync(script, args, { cwd: target, stdio: 'inherit' });
1410
+ if (child.error) throw child.error;
1411
+ if (child.status !== 0) {
1412
+ throw new Error(`Phase B installer failed (${child.status ?? 'unknown exit'})`);
1413
+ }
1414
+ return true;
1415
+ }
1389
1416
  const child = spawn(script, [], {
1390
1417
  cwd: target,
1391
1418
  detached: true,
@@ -1520,7 +1547,12 @@ ${openWorkframeBrowserBlockSh()}
1520
1547
  }
1521
1548
 
1522
1549
  function startInstallPs1(docker, nativeProfile, nativeAgentName, ports) {
1523
- return `$ErrorActionPreference = 'Stop'
1550
+ return `param(
1551
+ [switch]$NoBrowser,
1552
+ [switch]$NoPrompt
1553
+ )
1554
+
1555
+ $ErrorActionPreference = 'Stop'
1524
1556
  $Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
1525
1557
  Set-Location $Root
1526
1558
 
@@ -1533,7 +1565,9 @@ Write-Host " Project: ${nativeAgentName}" -ForegroundColor Cyan
1533
1565
  Write-Host '========================================' -ForegroundColor Cyan
1534
1566
  Write-Host ''
1535
1567
 
1536
- & "$Root\\scripts\\open-install-ui.ps1"
1568
+ if (-not $NoBrowser) {
1569
+ & "$Root\\scripts\\open-install-ui.ps1"
1570
+ }
1537
1571
 
1538
1572
  Write-Host 'Step 1/2 - Starting Docker stack...' -ForegroundColor Yellow
1539
1573
  ${composeUpPs1Block()}
@@ -1558,8 +1592,14 @@ Write-Host ' Workframe UI: http://127.0.0.1:${ports.ui}'
1558
1592
  Write-Host ' Hermes dashboard: http://127.0.0.1:${ports.dashboard}'
1559
1593
  Write-Host ''
1560
1594
  Write-Host 'LLM keys are configured in the onboarding UI - no Hermes TUI required.' -ForegroundColor DarkGray
1561
- ${openInstallBrowserBlockPs1()}
1562
- Read-Host 'Press Enter to close'
1595
+ if (-not $NoBrowser) {
1596
+ Write-Host ''
1597
+ Write-Host 'Opening Workframe setup wizard (/install)...' -ForegroundColor Green
1598
+ & "$Root\\scripts\\open-install-ui.ps1"
1599
+ }
1600
+ if (-not $NoPrompt) {
1601
+ Read-Host 'Press Enter to close'
1602
+ }
1563
1603
  `;
1564
1604
  }
1565
1605
 
@@ -1569,6 +1609,15 @@ set -euo pipefail
1569
1609
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
1570
1610
  cd "$ROOT"
1571
1611
 
1612
+ NO_BROWSER=0
1613
+ for arg in "$@"; do
1614
+ case "$arg" in
1615
+ --no-browser) NO_BROWSER=1 ;;
1616
+ --no-prompt) ;;
1617
+ *) echo "Unknown option: $arg" >&2; exit 2 ;;
1618
+ esac
1619
+ done
1620
+
1572
1621
  mkdir -p Agents Files
1573
1622
 
1574
1623
  echo ""
@@ -1578,7 +1627,9 @@ echo " Project: ${nativeAgentName}"
1578
1627
  echo "========================================"
1579
1628
  echo ""
1580
1629
 
1581
- "$ROOT/scripts/open-install-ui.sh" || true
1630
+ if [ "$NO_BROWSER" -eq 0 ]; then
1631
+ "$ROOT/scripts/open-install-ui.sh" || true
1632
+ fi
1582
1633
 
1583
1634
  echo "Step 1/2 — Starting Docker stack..."
1584
1635
  ${composeUpBashBlock()}
@@ -1598,7 +1649,11 @@ echo " Workframe UI: http://127.0.0.1:${ports.ui}"
1598
1649
  echo " Hermes dashboard: http://127.0.0.1:${ports.dashboard}/"
1599
1650
  echo ""
1600
1651
  echo "LLM keys are configured in the onboarding UI - no Hermes TUI required."
1601
- ${openInstallBrowserBlockSh()}
1652
+ if [ "$NO_BROWSER" -eq 0 ]; then
1653
+ echo ""
1654
+ echo "Opening Workframe setup wizard (/install)..."
1655
+ "$ROOT/scripts/open-install-ui.sh" || true
1656
+ fi
1602
1657
  `;
1603
1658
  }
1604
1659
 
@@ -1827,6 +1882,8 @@ function parseArgs(argv) {
1827
1882
  else if (a === '--run-docker-pull') args.runDockerPull = true;
1828
1883
  else if (a === '--allow-install-actions') args.installGuideOnly = false;
1829
1884
  else if (a === '--no-launch') args.noLaunch = true;
1885
+ else if (a === '--wait') args.waitForInstall = true;
1886
+ else if (a === '--no-browser') args.noBrowser = true;
1830
1887
  else if (a === '--slot') args.slot = Number(argv[++i]);
1831
1888
  else if (a.startsWith('--slot=')) args.slot = Number(a.split('=', 2)[1]);
1832
1889
  else if (a === '--deploy') args.deploy = argv[++i];
@@ -1867,6 +1924,8 @@ Flags:
1867
1924
  --allow-install-actions Allow installer to execute shell install actions
1868
1925
  -y, --yes Non-interactive defaults
1869
1926
  --no-launch Scaffold only; do not open Phase B installer
1927
+ --wait Run Phase B here and fail if installation fails
1928
+ --no-browser With --wait, do not open the system browser
1870
1929
  `);
1871
1930
  }
1872
1931
 
@@ -2799,9 +2858,14 @@ async function main() {
2799
2858
  console.log(`Bootstrap default: bootstrap-native (full pack: ${profiles.length} profiles)`);
2800
2859
  console.log(`SOUL seeds: ${seedProfiles.length} profiles + native SETUP playbook`);
2801
2860
  if (!args.ci && !args.noLaunch) {
2802
- if (launchPhaseBInstaller(target)) {
2861
+ if (launchPhaseBInstaller(target, {
2862
+ wait: Boolean(args.waitForInstall),
2863
+ noBrowser: Boolean(args.noBrowser),
2864
+ })) {
2803
2865
  console.log('');
2804
- console.log('Launching installer — browser opens /install immediately.');
2866
+ console.log(args.waitForInstall
2867
+ ? 'Installer completed successfully.'
2868
+ : 'Launching installer — browser opens /install immediately.');
2805
2869
  console.log(`Continue setup at http://127.0.0.1:${ports.ui}/install`);
2806
2870
  }
2807
2871
  } else if (args.noLaunch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-workframe",
3
- "version": "0.1.18",
3
+ "version": "0.1.21",
4
4
  "description": "Scaffold a Workframe + Hermes workspace with guided onboarding",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,9 +61,19 @@ copyTree(dist, UI_DEST);
61
61
 
62
62
  const pkgVersion = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
63
63
  const gitRef = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8', cwd: REPO_ROOT });
64
+ const bundledAt = new Date().toISOString();
65
+ const assetRevision = `${pkgVersion}-${Date.parse(bundledAt).toString(36)}`;
66
+ const bundledIndexPath = path.join(UI_DEST, 'index.html');
67
+ const bundledIndex = fs.readFileSync(bundledIndexPath, 'utf8').replace(
68
+ /((?:src|href)=["']\/assets\/[^"'?]+)(["'])/g,
69
+ `$1?v=${assetRevision}$2`,
70
+ );
71
+ fs.writeFileSync(bundledIndexPath, bundledIndex);
72
+
64
73
  const buildStamp = {
65
74
  package_version: pkgVersion,
66
- bundled_at: new Date().toISOString(),
75
+ bundled_at: bundledAt,
76
+ asset_revision: assetRevision,
67
77
  git_ref: gitRef.status === 0 ? gitRef.stdout.trim() : '',
68
78
  };
69
79
  fs.writeFileSync(path.join(UI_DEST, 'workframe-build.json'), `${JSON.stringify(buildStamp, null, 2)}\n`);
@@ -82,8 +82,10 @@ def forward_request(
82
82
  "content-length",
83
83
  "authorization",
84
84
  "x-api-key",
85
+ "accept-encoding",
85
86
  }
86
87
  }
88
+ upstream_headers["Accept-Encoding"] = "identity"
87
89
  upstream_headers.update(upstream_auth_header(provider, secret))
88
90
 
89
91
  req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
@@ -91,10 +93,16 @@ def forward_request(
91
93
  with urllib.request.urlopen(req, timeout=600) as resp:
92
94
  resp_body = resp.read()
93
95
  out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
96
+ content_encoding = resp.headers.get("Content-Encoding", "").strip()
97
+ if content_encoding:
98
+ out_headers["Content-Encoding"] = content_encoding
94
99
  return resp.status, out_headers, resp_body
95
100
  except urllib.error.HTTPError as exc:
96
101
  raw = exc.read()
97
102
  out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
103
+ content_encoding = exc.headers.get("Content-Encoding", "").strip()
104
+ if content_encoding:
105
+ out_headers["Content-Encoding"] = content_encoding
98
106
  return exc.code, out_headers, raw
99
107
 
100
108
 
@@ -213,6 +213,34 @@ def _llm_attribution_for_profile(profile: str, session_model: str = "") -> tuple
213
213
  return model, str(billing or "").strip()
214
214
 
215
215
 
216
+ def _session_run_attributions(profile: str, session_id: str) -> list[tuple[str, str]]:
217
+ """Completed-turn model/provider pairs in the same order as chat turns."""
218
+ prof = str(profile or "").strip()
219
+ sid = str(session_id or "").strip()
220
+ if not prof or not sid:
221
+ return []
222
+ conn = _srv()._workframe_db()
223
+ try:
224
+ rows = conn.execute(
225
+ """
226
+ SELECT li.model, li.provider
227
+ FROM runs r
228
+ JOIN run_line_items li ON li.run_id = r.run_id AND li.kind = 'llm_turn'
229
+ WHERE r.session_id = ? AND r.profile_slug = ? AND r.status = 'completed'
230
+ ORDER BY r.created_at ASC, li.created_at ASC, li.line_item_id ASC
231
+ """,
232
+ (sid, prof),
233
+ ).fetchall()
234
+ except sqlite3.Error:
235
+ return []
236
+ finally:
237
+ conn.close()
238
+ return [
239
+ (str(row["model"] or "").strip(), str(row["provider"] or "").strip())
240
+ for row in rows
241
+ ]
242
+
243
+
216
244
  def _latest_session_id(profile: str) -> str:
217
245
  """Most recent session — prefer active (ended_at IS NULL), then by started_at."""
218
246
  db = _srv()._profile_dir(profile) / "state.db"
@@ -441,7 +469,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
441
469
  _srv()._runtime_template_slug(profile) if _srv()._is_runtime_profile_slug(profile) else profile
442
470
  )
443
471
  display = _srv()._profile_display_name(profile)
444
- attr_model, attr_provider = _llm_attribution_for_profile(profile)
472
+ run_attributions = _session_run_attributions(profile, sid)
473
+ agent_turn_index = 0
445
474
  try:
446
475
  raw = conn.execute(
447
476
  """
@@ -483,12 +512,14 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
483
512
  if current and current["role"] == "agent":
484
513
  current["segments"].extend(segments)
485
514
  current["tokens"] += tokens
486
- if attr_model:
487
- current["model"] = attr_model
488
- current["llm_provider"] = attr_provider
489
515
  else:
490
516
  if current:
491
517
  turns.append(current)
518
+ if agent_turn_index < len(run_attributions):
519
+ attr_model, attr_provider = run_attributions[agent_turn_index]
520
+ else:
521
+ attr_model, attr_provider = "", ""
522
+ agent_turn_index += 1
492
523
  current = {
493
524
  "id": f"turn-{m['id']}",
494
525
  "authorId": author_profile,
@@ -497,8 +528,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
497
528
  "segments": list(segments),
498
529
  "timestamp": ts,
499
530
  "tokens": tokens,
500
- "model": attr_model,
501
- "llm_provider": attr_provider,
531
+ **({"model": attr_model} if attr_model else {}),
532
+ **({"llm_provider": attr_provider} if attr_provider else {}),
502
533
  }
503
534
  if current:
504
535
  turns.append(current)
@@ -391,6 +391,22 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
391
391
  raise ValueError("session_id required")
392
392
  if not text:
393
393
  raise ValueError("text required")
394
+ # Bootstrap before reading the model block. Existing generated profiles may
395
+ # need provider/model migrations; reading first dispatches one stale turn.
396
+ bootstrapped = _srv()._bootstrap_profile_providers(prof, _triggering_user, _workspace_id)
397
+ # Native/template profiles can still contain the installer default
398
+ # (provider=openrouter + public base_url). A user vault credential must
399
+ # always be consumed through Workframe's authenticated proxy.
400
+ if _triggering_user:
401
+ reconciled = _srv()._reconcile_profile_llm_for_user(
402
+ prof, _triggering_user, _workspace_id,
403
+ )
404
+ else:
405
+ reconciled = False
406
+ if bootstrapped and not reconciled:
407
+ # Hermes caches config.yaml. Reload before dispatching so the first
408
+ # turn after a repair or model save cannot use stale routing.
409
+ _srv()._reload_runtime_profile_gateway(prof, wait_healthy=True)
394
410
  model_block = _srv()._read_model_block(prof)
395
411
  llm_provider = _srv()._llm_billing_provider(
396
412
  prof, user_id=_triggering_user, workspace_id=_workspace_id, block=model_block,
@@ -588,6 +588,33 @@ def _migrate_v13_workspace_members_in_spaces(conn: sqlite3.Connection) -> None:
588
588
  VALUES ('13', 'workspace members joined to all space rooms', datetime('now'))
589
589
  """,
590
590
  )
591
+ def _migrate_v15_message_reactions(conn: sqlite3.Connection) -> None:
592
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '15' LIMIT 1").fetchone():
593
+ return
594
+ conn.execute(
595
+ """
596
+ CREATE TABLE IF NOT EXISTS message_reactions (
597
+ scope_key TEXT NOT NULL,
598
+ message_id TEXT NOT NULL,
599
+ user_id TEXT NOT NULL,
600
+ emoji TEXT NOT NULL,
601
+ created_at TEXT NOT NULL,
602
+ PRIMARY KEY (scope_key, message_id, user_id, emoji)
603
+ )
604
+ """
605
+ )
606
+ conn.execute(
607
+ """
608
+ CREATE INDEX IF NOT EXISTS idx_message_reactions_scope_message
609
+ ON message_reactions(scope_key, message_id, created_at)
610
+ """
611
+ )
612
+ conn.execute(
613
+ """
614
+ INSERT INTO schema_migrations (version, description, applied_at)
615
+ VALUES ('15', 'message reactions for room and agent-DM chat surfaces', datetime('now'))
616
+ """,
617
+ )
591
618
  def _migrate_v8_room_avatars(conn: sqlite3.Connection) -> None:
592
619
  if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '8' LIMIT 1").fetchone():
593
620
  return
@@ -738,6 +765,7 @@ def ensure_workframe_db_schema(open_db: OpenDb) -> None:
738
765
  _migrate_v11_delegation_kanban_boards(conn)
739
766
  _migrate_v12_adopt_install_keys_to_owners(conn)
740
767
  _migrate_v13_workspace_members_in_spaces(conn)
768
+ _migrate_v15_message_reactions(conn)
741
769
  conn.commit()
742
770
  finally:
743
771
  conn.close()
@@ -750,6 +778,7 @@ def ensure_oauth_pending_migrations(open_db: OpenDb) -> None:
750
778
  _migrate_v11_delegation_kanban_boards(conn)
751
779
  _migrate_v12_adopt_install_keys_to_owners(conn)
752
780
  _migrate_v13_workspace_members_in_spaces(conn)
781
+ _migrate_v15_message_reactions(conn)
753
782
  conn.commit()
754
783
  finally:
755
784
  conn.close()
@@ -197,6 +197,38 @@ class AdminRoutesMixin:
197
197
  content_b64 = str(body.get("content_base64") or body.get("content") or "")
198
198
  self._json(200, srv.file_upload_binary(rel, content_b64))
199
199
 
200
+ def _route_post_files_archive(self, body: dict) -> None:
201
+ srv = _srv()
202
+ if not srv._check_auth(self):
203
+ self._json(401, {"error": "unauthorized"})
204
+ return
205
+ paths = body.get("paths")
206
+ try:
207
+ archive = srv.files_archive(paths)
208
+ except PermissionError as exc:
209
+ self._json(403, {"ok": False, "error": "protected_file", "detail": str(exc)})
210
+ return
211
+ except ValueError as exc:
212
+ self._json(400, {"ok": False, "error": "invalid_selection", "detail": str(exc)})
213
+ return
214
+ self._send(200, archive, "application/zip")
215
+
216
+ def _route_post_files_delete(self, body: dict) -> None:
217
+ srv = _srv()
218
+ if not srv._check_auth(self):
219
+ self._json(401, {"error": "unauthorized"})
220
+ return
221
+ paths = body.get("paths")
222
+ try:
223
+ result = srv.files_delete(paths)
224
+ except PermissionError as exc:
225
+ self._json(403, {"ok": False, "error": "protected_file", "detail": str(exc)})
226
+ return
227
+ except ValueError as exc:
228
+ self._json(400, {"ok": False, "error": "invalid_selection", "detail": str(exc)})
229
+ return
230
+ self._json(200, result)
231
+
200
232
  def _route_get_admin_vault_status(self, qs: dict[str, list[str]]) -> None:
201
233
  srv = _srv()
202
234
  if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
@@ -529,13 +529,16 @@ class AuthRoutesMixin:
529
529
  )
530
530
  display = str(body.get("display_name") or "").strip() if "display_name" in body else ""
531
531
  tagline = str(body.get("tagline") or "").strip() if "tagline" in body else ""
532
- srv._seed_native_user_overlay(
533
- runtime,
532
+ srv._apply_profile_identity(
534
533
  native_slug,
535
534
  display_name=display,
536
535
  tagline=tagline,
537
- user_soul=soul,
538
536
  )
537
+ if soul:
538
+ result = srv.profile_soul_set(native_slug, soul)
539
+ if not result.get("ok"):
540
+ self._json(400, result)
541
+ return
539
542
  profile_patch: dict[str, Any] = {}
540
543
  if display:
541
544
  profile_patch["display_name"] = display
@@ -552,6 +555,5 @@ class AuthRoutesMixin:
552
555
  stamp = datetime.now(timezone.utc).isoformat()
553
556
  row_patch = {**avatar_patch, "updated_at": stamp}
554
557
  srv._upsert_agent_registry_row(native_slug, row_patch)
555
- srv._upsert_agent_registry_row(runtime, row_patch)
556
558
  srv._sync_agent_profile_db(native_slug, {"avatar_url": avatar_patch.get("avatar_url", "")})
557
559
  self._json(200, {"ok": True, "runtime_profile": runtime})
@@ -223,17 +223,12 @@ class ChatRoutesMixin:
223
223
  self._json(400, {"error": "model required"})
224
224
  return
225
225
  billing_provider = str(body.get("billing_provider", "")).strip()
226
- # A member may tune the model used by their proxy, but cannot mutate
227
- # the shared agent template. The template remains the default model
228
- # owned by the agent; this maps ordinary member writes to u-* runtime.
229
- if profile and not selection_only and not srv._is_runtime_profile_slug(profile):
230
- if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
231
- if not workspace_id:
232
- workspaces = srv._get_user_workspaces(user_id)
233
- current = srv._resolve_current_workspace(user_id, workspaces)
234
- workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
235
- profile = srv._runtime_profile_slug(user_id, profile)
236
- srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
226
+ # Provider/model are shared agent settings. Credentials remain scoped
227
+ # to the acting user or company, so only workspace admins may mutate
228
+ # the agent template; the backend canonicalizes any stale u-* request.
229
+ if not selection_only and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
230
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
231
+ return
237
232
  self._json(
238
233
  200,
239
234
  hermes_model_set(
@@ -267,14 +262,7 @@ class ChatRoutesMixin:
267
262
  chain = body.get("chain", [])
268
263
  selection_only = bool(body.get("selection_only"))
269
264
  user_id = str(getattr(self, "auth_user", "") or "")
270
- if profile and not selection_only and not srv._is_runtime_profile_slug(profile):
271
- if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
272
- if not workspace_id:
273
- workspaces = srv._get_user_workspaces(user_id)
274
- current = srv._resolve_current_workspace(user_id, workspaces)
275
- workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
276
- profile = srv._runtime_profile_slug(user_id, profile)
277
- srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
265
+ workspace_id = str(body.get("workspace_id", "")).strip()
278
266
  if not isinstance(chain, list):
279
267
  self._json(400, {"error": "chain must be a list"})
280
268
  return
@@ -288,6 +276,7 @@ class ChatRoutesMixin:
288
276
  chain,
289
277
  selection_only=selection_only,
290
278
  user_id=user_id,
279
+ workspace_id=workspace_id,
291
280
  ),
292
281
  )
293
282
 
@@ -546,9 +535,7 @@ class ChatRoutesMixin:
546
535
  return
547
536
  template = resolve_validated_profile(profile_bootstrap_post.group(1))
548
537
  user_id = str(getattr(self, "auth_user", "") or "").strip()
549
- if srv._is_native_profile(template) and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
550
- self._json(403, {"ok": False, "error": "native_agent_identity_owner_only"})
551
- return
538
+ can_manage_identity = srv._role_allows(self, srv.OWNER_ADMIN_ROLES)
552
539
  workspace_id = str(body.get("workspace_id") or "").strip()
553
540
  if not workspace_id and user_id:
554
541
  workspaces = srv._get_user_workspaces(user_id)
@@ -558,10 +545,14 @@ class ChatRoutesMixin:
558
545
  self._json(400, {"error": "workspace_id and session required"})
559
546
  return
560
547
  model = str(body.get("model") or "").strip()
561
- soul = str(body.get("soul") or "").strip()
562
- display_name = str(body.get("display_name") or body.get("room_name") or "").strip()
563
- role = str(body.get("role") or "").strip()
564
- tagline = str(body.get("tagline") or "").strip()
548
+ soul = str(body.get("soul") or "").strip() if can_manage_identity else ""
549
+ display_name = (
550
+ str(body.get("display_name") or body.get("room_name") or "").strip()
551
+ if can_manage_identity
552
+ else ""
553
+ )
554
+ role = str(body.get("role") or "").strip() if can_manage_identity else ""
555
+ tagline = str(body.get("tagline") or "").strip() if can_manage_identity else ""
565
556
  lane = bootstrap_agent_dm_lane(
566
557
  user_id,
567
558
  workspace_id,
@@ -8,7 +8,6 @@ from typing import Any
8
8
  import action_proxy
9
9
  import internal_proxy_auth
10
10
  import llm_proxy
11
- import openrouter_catalog
12
11
  import platform_auth
13
12
  import run_surface_wiring
14
13
 
@@ -125,7 +124,8 @@ class ProviderRoutesMixin:
125
124
  self._json(401, {"ok": False, "error": "no_session"})
126
125
  return
127
126
  workspace_id = qs.get("workspace_id", [""])[0]
128
- self._json(200, srv.list_user_providers(user_id, workspace_id))
127
+ credential_scope = qs.get("credential_scope", ["effective"])[0]
128
+ self._json(200, srv.list_user_providers(user_id, workspace_id, credential_scope))
129
129
 
130
130
  def _route_post_me_credentials(self, body: dict) -> None:
131
131
  srv = _srv()
@@ -191,19 +191,18 @@ class ProviderRoutesMixin:
191
191
  self._json(500, {"ok": False, "error": f"db_error: {exc}"})
192
192
  return
193
193
  srv._invalidate_user_llm_picker_cache(user_id)
194
+ srv._revoke_runtime_llm_leases(payer_user_id=user_id, provider=provider)
194
195
  self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
195
- health: dict[str, Any] = {}
196
- if provider == "openrouter":
197
- secret_probe = srv._credential_secret(
198
- {
199
- "credential_ref": cred_ref,
200
- "scope": "user",
201
- "user_id": user_id,
202
- },
203
- user_id,
204
- )
205
- if secret_probe:
206
- health = openrouter_catalog.probe_account(secret_probe)
196
+ # Credential persistence must not depend on an upstream network round-trip.
197
+ # OpenRouter's account endpoints can each consume their full timeout when a
198
+ # self-hosted install has restricted egress, leaving the settings UI stuck
199
+ # in "Saving…" even though the encrypted credential is already committed.
200
+ # Model/catalog requests provide the first real provider health signal.
201
+ health: dict[str, Any] = {
202
+ "ok": True,
203
+ "provider": provider,
204
+ "status": "stored",
205
+ }
207
206
  try:
208
207
  srv._bootstrap_model_after_llm_connect(user_id, str(body.get("workspace_id") or ""), provider)
209
208
  except (OSError, RuntimeError, ValueError) as exc: