tide-commander 1.139.2 → 1.141.0

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 (65) hide show
  1. package/CHANGELOG.md +5432 -0
  2. package/dist/assets/{BossLogsModal-BuknVw0t.js → BossLogsModal-DUh9mA0g.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-Bk_GVUor.js → BossSpawnModal-CGEysDmo.js} +1 -1
  4. package/dist/assets/{ControlsModal-YxXtwKJV.js → ControlsModal-DKlL4BtE.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-CblkdNqt.js → DockerLogsModal-Dmbe01S9.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-D_jdlmTI.js → EmbeddedEditor-Bx_zcxVC.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-CGXFB4Xr.js → GmailOAuthSetup-CIPQc_WY.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-Btgu3TOp.js → GoogleOAuthSetup-CnOo59V4.js} +1 -1
  9. package/dist/assets/{IframeModal-yvc5X_Yh.js → IframeModal-ByDSCNMW.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-zQ7-X7PF.js → IntegrationsPanel-CaiJ__1J.js} +2 -2
  11. package/dist/assets/{LogViewerModal-DGzIeeJQ.js → LogViewerModal-C-w4cekL.js} +1 -1
  12. package/dist/assets/{MonitoringModal-DySuIczM.js → MonitoringModal-DIJd8oHM.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-Dq5ZLh0W.js → PM2LogsModal-BFelxtQL.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-D_6yMJIx.js → RestoreArchivedAreaModal-BYTl2kIG.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-tZKB12LU.js → Scene2DCanvas-CJgfmvZA.js} +1 -1
  16. package/dist/assets/{SceneManager-BeJLz_RT.js → SceneManager-dbNZbSKp.js} +1 -1
  17. package/dist/assets/{SkillsPanel-gPP_MHFb.js → SkillsPanel-CwVwPU5t.js} +2 -2
  18. package/dist/assets/{SlackMultiInstanceSetup-C7Qvm39W.js → SlackMultiInstanceSetup-DgjwCx-F.js} +1 -1
  19. package/dist/assets/{SpawnModal-D3-Z5mzv.js → SpawnModal-DLdyhgtN.js} +1 -1
  20. package/dist/assets/{StatisticsModal-DCAF9Dsa.js → StatisticsModal-Dd1yh0YT.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-DTbryouu.js → SubordinateAssignmentModal-bGplpTfz.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-i9h5lRli.js → TriggerManagerPanel-BmdbFUJB.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-DY8YA4bD.js → WorkflowEditorPanel-BRGVEv4g.js} +1 -1
  24. package/dist/assets/{area-logos-CFnhy-W8.js → area-logos-CPBT7k6a.js} +1 -1
  25. package/dist/assets/{index-Cjtcr4_w.js → index-BIfX5_Xq.js} +6 -6
  26. package/dist/assets/index-C09uD--W.js +2 -0
  27. package/dist/assets/{index-DAlpCHwk.js → index-Cd31ErRd.js} +1 -1
  28. package/dist/assets/{index-B1ZW-nQg.js → index-CjdSuvf1.js} +1 -1
  29. package/dist/assets/{index-BCTUaC9_.js → index-Clk0mGIC.js} +1 -1
  30. package/dist/assets/index-DIWScVQf.js +1 -0
  31. package/dist/assets/{index-ochVZ1qG.js → index-DPPtFIs3.js} +3 -3
  32. package/dist/assets/{index-CC0xW5So.js → index-DX8uai_5.js} +3 -3
  33. package/dist/assets/index-YDzuES6L.js +2 -0
  34. package/dist/assets/main-BCym1dpG.css +1 -0
  35. package/dist/assets/main-CAE1L3Sb.js +256 -0
  36. package/dist/assets/{web-KIxT1AKQ.js → web-B0Nj3gOp.js} +1 -1
  37. package/dist/assets/{web-f3jpYwOI.js → web-B5mCc_qm.js} +1 -1
  38. package/dist/assets/{web-CQHAjGLu.js → web-CaEzTKao.js} +1 -1
  39. package/dist/index.html +2 -2
  40. package/dist/locales/en/config.json +9 -1
  41. package/dist/src/packages/server/data/builtin-skills/browser-control.js +7 -5
  42. package/dist/src/packages/server/data/builtin-skills/create-building.js +7 -2
  43. package/dist/src/packages/server/data/builtin-skills/http-requests.js +51 -0
  44. package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
  45. package/dist/src/packages/server/data/builtin-skills/release-pipeline.js +10 -1
  46. package/dist/src/packages/server/data/index.js +65 -0
  47. package/dist/src/packages/server/routes/browser.js +4 -1
  48. package/dist/src/packages/server/routes/http-requests.js +148 -0
  49. package/dist/src/packages/server/routes/index.js +3 -1
  50. package/dist/src/packages/server/routes/system.js +30 -0
  51. package/dist/src/packages/server/services/building-service.js +5 -1
  52. package/dist/src/packages/server/services/claude-usage-service.js +101 -3
  53. package/dist/src/packages/server/services/http-requests-service.js +450 -0
  54. package/dist/src/packages/server/services/index.js +1 -0
  55. package/dist/src/packages/server/websocket/handler.js +2 -1
  56. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +75 -0
  57. package/dist/src/packages/shared/building-types.js +1 -0
  58. package/dist/src/packages/shared/http-requests-types.js +7 -0
  59. package/dist/src/packages/shared/types.js +1 -0
  60. package/package.json +3 -2
  61. package/dist/assets/index-B5TMKZMf.js +0 -2
  62. package/dist/assets/index-BL0c4Fee.js +0 -1
  63. package/dist/assets/index-C9wQ_Bkk.js +0 -2
  64. package/dist/assets/main-CpuU-ex-.js +0 -243
  65. package/dist/assets/main-d2YCqass.css +0 -1
@@ -1 +1 @@
1
- import{cP as t}from"./main-CpuU-ex-.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
1
+ import{cQ as t}from"./main-CAE1L3Sb.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
@@ -1 +1 @@
1
- import{cP as a}from"./main-CpuU-ex-.js";import{ImpactStyle as i,NotificationType as r}from"./index-C9wQ_Bkk.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{cQ as a}from"./main-CAE1L3Sb.js";import{ImpactStyle as i,NotificationType as r}from"./index-YDzuES6L.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
@@ -1 +1 @@
1
- import{cP as s}from"./main-CpuU-ex-.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{cQ as s}from"./main-CAE1L3Sb.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
package/dist/index.html CHANGED
@@ -22,10 +22,10 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-CpuU-ex-.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-CAE1L3Sb.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
28
- <link rel="stylesheet" crossorigin href="/assets/main-d2YCqass.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-BCym1dpG.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -581,7 +581,15 @@
581
581
  "close": "Close",
582
582
  "stuckHint": "Taking longer than expected?",
583
583
  "reload": "Reload",
584
- "recheck": "Recheck"
584
+ "recheck": "Recheck",
585
+ "updated": "Updated to v{{version}}",
586
+ "viewChangelog": "View changelog",
587
+ "changelogTitle": "What's new in v{{version}}",
588
+ "changelogTitleAll": "Changelog",
589
+ "changelogLoading": "Loading changelog…",
590
+ "changelogError": "Couldn't load the changelog.",
591
+ "changelogEmpty": "No release notes for this version.",
592
+ "viewOnGithub": "View on GitHub"
585
593
  },
586
594
  "shortcuts": {
587
595
  "pressKeys": "Press keys...",
@@ -31,10 +31,12 @@ matter what's focused. \`/console\`, \`/network\` and \`/errors\` are SCOPED to
31
31
  - **Multiple agents can drive different tabs at the same time.** Each agent must target its
32
32
  own tab by \`tab\`/\`tabId\`; never rely on "the active tab".
33
33
  - **Do NOT call \`/api/browser/tab/activate\` just to drive.** Clicks/typing/reads work on a
34
- **background, unfocused tab** (the user may be on another screen/monitor) — activating
35
- steals their focus and, with multiple agents, makes the tab fight over the foreground.
36
- Activate ONLY if the user explicitly asks, or you need a full-viewport screenshot
37
- (element-clip screenshots don't need it either).
34
+ **background, unfocused tab** (the user may be on another screen/monitor) — activate ONLY
35
+ if the user explicitly asks, or you need a full-viewport screenshot (element-clip
36
+ screenshots don't need it either). Note: \`/tab/activate\` now makes the tab the active
37
+ tab of its window WITHOUT stealing the user's OS focus (their current app stays in front)
38
+ enough for \`captureVisibleTab\`. Pass \`"focusWindow":true\` to ALSO raise the window to the
39
+ foreground; that interrupts whatever the user is doing, so use it only when they ask.
38
40
 
39
41
  ## Reads (always available)
40
42
  - \`POST /api/browser/dom\` \`{selector, all?}\` → outerHTML/text/box/styles + **\`field\`** (live \`value\`/\`checked\`/\`selectedText\`/\`disabled\` for input/textarea/select). Read form state from \`field\`, NOT from the value attribute (React often doesn't mirror it) and NOT via \`evaluate\` (its content-script fallback is CSP-blocked). Bulky \`<svg>\` icon markup is stripped to a compact \`<svg data-icon>\` placeholder by default (token saver) — pass \`"keepSvg":true\` to keep it.
@@ -54,7 +56,7 @@ matter what's focused. \`/console\`, \`/network\` and \`/errors\` are SCOPED to
54
56
  - \`POST /api/browser/wait\` \`{selector | text | ms}\` · \`/evaluate\` \`{expression}\` → \`{value}\`
55
57
  - \`POST /api/browser/drag\` \`{from, to}\` · \`/dialog\` \`{accept?, promptText?}\` · \`/cdp-raw\` \`{method, params?}\`
56
58
  - \`POST /api/browser/batch\` \`{tab|tabId, steps:[{cmd, …}]}\` — runs ANY steps in order (mixed click/type/wait/…), stops at first failure unless the step has \`continueOnError\`. Use for non-form multi-step flows; for pure form-filling prefer \`/fill\`.
57
- - Tabs: \`POST /api/browser/tab/{open,close,activate}\`
59
+ - Tabs: \`POST /api/browser/tab/{open,close,activate}\` — \`tab/open\` opens in the BACKGROUND by default (drive/read it there without interrupting the user); pass \`"active":true\` only to actually switch the user to the new tab. \`tab/activate\` won't steal OS focus unless you pass \`"focusWindow":true\`.
58
60
 
59
61
  Every call returns \`{ok:true, result}\` or \`{ok:false, error}\`. **If you pipe to \`jq\`, use
60
62
  the right paths — wrong paths return \`null\`, which is NOT the API failing** (it's your
@@ -47,7 +47,7 @@ ${BT}name${BT}, ${BT}type${BT}, and ${BT}position${BT} are always required. Styl
47
47
  ${BT3}typescript
48
48
  {
49
49
  name: string, // Display name
50
- type: 'server'|'link'|'database'|'docker'|'monitor'|'folder'|'boss'|'terminal'|'tests',
50
+ type: 'server'|'link'|'database'|'docker'|'monitor'|'folder'|'boss'|'terminal'|'tests'|'http',
51
51
  position: { x: number, z: number },
52
52
  style?: 'server-rack'|'tower'|'dome'|'pyramid'|'desktop'
53
53
  | 'filing-cabinet'|'satellite'|'crystal'|'factory'|'command-center',
@@ -58,7 +58,7 @@ ${BT3}typescript
58
58
  docker?: { ... }, // Docker type — see below
59
59
  database?: { connections: [...] }, // Database type
60
60
  terminal?: { ... }, // Terminal type
61
- folderPath?: string, // Folder + tests types — required
61
+ folderPath?: string, // Folder + tests + http types — required
62
62
  urls?: [{ label, url }], // Link type — required
63
63
  commands?: { start, stop, restart, healthCheck, logs }, // Non-PM2 server custom commands
64
64
  subordinateBuildingIds?: string[], // Boss type
@@ -221,6 +221,11 @@ Requires ${BT}ttyd${BT} installed; ${BT}saveSession: true${BT} also needs ${BT}t
221
221
  - ${BT}{"name":"Docs","type":"link","style":"tower","position":{"x":10,"z":0},"urls":[{"label":"Internal Wiki","url":"https://wiki.example.com"}]}${BT}
222
222
  - ${BT}{"name":"Host Metrics","type":"monitor","style":"satellite","position":{"x":12,"z":4}}${BT}
223
223
 
224
+ ### Tests / HTTP requests
225
+
226
+ - Tests browser (scan + run a folder's test suite): ${BT}{"name":"Core Tests","type":"tests","style":"dome","position":{"x":6,"z":3},"folderPath":"/home/user/projects/core"}${BT}
227
+ - HTTP request tests (IntelliJ-style .http file runner; picks up ${BT}http-client.env.json${BT} / ${BT}http-client.private.env.json${BT} environments in the folder): ${BT}{"name":"Core API Requests","type":"http","style":"satellite","position":{"x":7,"z":3},"folderPath":"/home/user/projects/core/http-requests"}${BT}
228
+
224
229
  ### Boss
225
230
 
226
231
  Subordinates must already exist — get their IDs from ${BT}GET /api/buildings${BT} first.
@@ -0,0 +1,51 @@
1
+ export const httpRequests = {
2
+ slug: 'http-requests',
3
+ name: 'HTTP Request Tests',
4
+ description: 'Browse and fire IntelliJ-style .http request files via the Tide Commander http-requests API; results render as rich cards in the terminal (method, status, timing, body preview).',
5
+ allowedTools: ['Bash(curl:*)', 'Bash(jq:*)'],
6
+ // Not assigned to anyone by default — enable it per agent/class in the Skills panel.
7
+ assignedAgentClasses: [],
8
+ content: `# HTTP Request Tests
9
+
10
+ Run requests from IntelliJ-style \`.http\` / \`.rest\` files through \`/api/http-requests/*\` (never by hand-crafting the HTTP call yourself) so each result is persisted to the building's history and renders as a **rich card** in the user's terminal — method, status pill, timing, size and a body preview instead of raw JSON.
11
+
12
+ Use this when asked to fire/test HTTP requests from a project's \`.http\` files, smoke-test API endpoints defined there, or report on past request runs. The folder usually comes from an \`http\`-type building — find it with \`GET /api/buildings\` (look for \`"type":"http"\`, use its \`folderPath\`).
13
+
14
+ All calls use the API Calling Convention scaffolding (host, \`-H "X-Auth-Token: ..."\`). No exclamation marks in commands.
15
+
16
+ ## 1. Scan — list files, requests and environments
17
+
18
+ \`POST /api/http-requests/scan\`
19
+ \`\`\`json
20
+ {"path":"/abs/path/to/http-folder"}
21
+ \`\`\`
22
+ Returns every parsed file with its requests (0-based \`index\`, \`name\`, \`method\`, \`url\`, \`variables\`) plus \`environments\` (from \`http-client.env.json\` / \`http-client.private.env.json\`) and \`envFiles\`. Pipe through \`jq\` to keep it short, e.g. \`jq '{environments, files: [.files[] | {relFile, requests: [.requests[] | {index, name, method}]}]}'\`.
23
+
24
+ ## 2. Run one request
25
+
26
+ \`POST /api/http-requests/run\`
27
+ \`\`\`json
28
+ {"agentId":"YOUR_AGENT_ID","path":"/abs/path/to/http-folder","relFile":"payment-order.http","requestIndex":2,"env":"dev"}
29
+ \`\`\`
30
+ - \`agentId\` (IMPORTANT) — your agent ID from the system prompt. With it, a live card appears in the user's terminal under your curl the moment the request fires (spinner → method/status/time/body, with Re-run and copy-curl buttons). Without it, nothing renders.
31
+ - \`requestIndex\` — the request's 0-based \`index\` from the scan.
32
+ - \`env\` (optional but usually required) — environment name for \`{{variable}}\` resolution; omit only when the file has no variables. \`unresolvedVariables\` in the response tells you what was missing.
33
+
34
+ Synchronous: the response IS the result (\`ok\`, \`status\`, \`timeMs\`, \`sizeBytes\`, \`headers\`, \`body\`, resolved \`request\`, \`runId\`). 60s timeout, 2 MB body cap.
35
+
36
+ Silence the response with \`-o /dev/null\` if you don't need to inspect it (the card already shows everything to the user), or print it as-is (plain \`curl -s\`, no jq filter) — printed run JSON also renders as a card. To READ a field yourself, fetch \`GET /runs/RUN_ID\` afterwards and jq that.
37
+
38
+ ## 3. Run several / "run all in a file"
39
+
40
+ Run each request as its OWN curl command (one Bash call per request, agentId included) so every run gets its own live card in the terminal. Avoid shell for-loops — a loop is a single Bash line, so only the last run's card would attach to it. Do not parallelize.
41
+
42
+ ## 4. History
43
+
44
+ \`GET /api/http-requests/history?folder=/abs/path&limit=20\` — recent executed requests for that folder (newest first). Printing this JSON also renders as a card (compact list of runs). \`GET /api/http-requests/runs/RUN_ID\` returns one stored run with the full request/response detail.
45
+
46
+ ## Rules
47
+ - A non-2xx status or \`"ok":false\` (connection refused, timeout) is a **normal result to report**, not an API error — summarize which requests passed/failed and why.
48
+ - Prefer the building's configured folder; for ad-hoc paths anything under the home directory works.
49
+ - Requests hit real endpoints. Fire GETs freely; for POST/PUT/PATCH/DELETE against non-test systems, confirm with the user first unless they explicitly asked.
50
+ - After a batch, finish with a short written summary (counts + failures); the cards complement it, they don't replace it.`,
51
+ };
@@ -29,6 +29,7 @@ import { workflowBuilder } from './workflow-builder.js';
29
29
  import { backupRestore } from './backup-restore.js';
30
30
  import { browserControl } from './browser-control.js';
31
31
  import { runTests } from './run-tests.js';
32
+ import { httpRequests } from './http-requests.js';
32
33
  /**
33
34
  * All built-in skills that ship with Tide Commander
34
35
  */
@@ -57,6 +58,7 @@ export const BUILTIN_SKILLS = [
57
58
  backupRestore,
58
59
  browserControl,
59
60
  runTests,
61
+ httpRequests,
60
62
  ];
61
63
  /**
62
64
  * Get the ID for a built-in skill based on its slug
@@ -62,6 +62,13 @@ git pull --rebase origin $(git branch --show-current)
62
62
  \`\`\`
63
63
  On conflicts: STOP immediately and report. Do NOT auto-resolve.
64
64
 
65
+ Verify the changelog packaging contract (fail fast — before doing any build/publish work):
66
+
67
+ \`\`\`bash
68
+ npm pkg get files | grep -q '"CHANGELOG.md"' || echo "MISSING: CHANGELOG.md not in package.json files"
69
+ \`\`\`
70
+ \`CHANGELOG.md\` MUST be listed in package.json \`files\`. It ships in the npm tarball and is served by the server at \`GET /api/system/changelog\`; the in-app changelog (post-update banner + Settings → About) reads this local packaged file instead of the GitHub API, which 403'd on rate limits (60 req/hour). If the check prints \`MISSING\`: STOP and report — do NOT publish, or the in-app changelog breaks.
71
+
65
72
  ### Phase 2: Quality Gates (3 parallel sub-agents)
66
73
 
67
74
  Spawn all 3 Agent calls in a single response, each using the curl template above with its command, checking output and exitCode, and instructed not to fix any issues:
@@ -105,7 +112,7 @@ npm version <patch|minor|major> --no-git-tag-version
105
112
  git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~20")..HEAD
106
113
  \`\`\`
107
114
 
108
- Add the new version entry at the top of \`CHANGELOG.md\` (below the header), Keep a Changelog format. Only include sections that have entries; write concise, user-facing descriptions.
115
+ Add the new version entry at the top of \`CHANGELOG.md\` (directly below the \`# Changelog\` header), Keep a Changelog format. Only include sections that have entries; write concise, user-facing descriptions. The changelog is a committed, packaged (package.json \`files\`), server-served artifact — it MUST be updated every release so the bundled file stays current.
109
116
 
110
117
  \`\`\`markdown
111
118
  ## [X.Y.Z] - YYYY-MM-DD
@@ -123,6 +130,8 @@ Add the new version entry at the top of \`CHANGELOG.md\` (below the header), Kee
123
130
  - (from \`remove:\` or \`delete:\` commits)
124
131
  \`\`\`
125
132
 
133
+ CRITICAL — keep the per-version header EXACTLY \`## [X.Y.Z] - YYYY-MM-DD\` (Keep a Changelog): one header per release, newest at the top. The in-app changelog modal extracts a single release by matching the line \`## [X.Y.Z]\`; any other header format breaks the per-version view (the full-changelog view still works). Do NOT drop the \`[\` \`]\` brackets, the version number, or change the \` - YYYY-MM-DD\` suffix.
134
+
126
135
  ### Phase 6: Commit, Tag, Push (run yourself — sequential git ops)
127
136
 
128
137
  \`\`\`bash
@@ -923,3 +923,68 @@ export function saveTestRun(run) {
923
923
  log.error(' Failed to save test run:', err);
924
924
  }
925
925
  }
926
+ // ============================================================================
927
+ // HTTP Run History (persisted to http-runs/ — one file per run + an index)
928
+ // Mirrors the test-run history above for the "http" (.http requests) building.
929
+ // ============================================================================
930
+ const HTTP_RUNS_DIR = path.join(DATA_DIR, 'http-runs');
931
+ const HTTP_RUNS_INDEX_FILE = path.join(HTTP_RUNS_DIR, 'index.json');
932
+ const MAX_HTTP_RUNS = 300;
933
+ function ensureHttpRunsDir() {
934
+ if (!fs.existsSync(HTTP_RUNS_DIR)) {
935
+ fs.mkdirSync(HTTP_RUNS_DIR, { recursive: true });
936
+ log.log(` Created http runs directory: ${HTTP_RUNS_DIR}`);
937
+ }
938
+ }
939
+ function getHttpRunFile(runId) {
940
+ return path.join(HTTP_RUNS_DIR, `${runId}.json`);
941
+ }
942
+ function summarizeHttpRun(run) {
943
+ const { result: _result, ...summary } = run;
944
+ return summary;
945
+ }
946
+ /** List recent http runs (newest first); optionally scoped to one folder. */
947
+ export function loadHttpRunSummaries(limit = 50, folder) {
948
+ ensureHttpRunsDir();
949
+ const data = safeReadJsonSync(HTTP_RUNS_INDEX_FILE, 'Http runs index');
950
+ let runs = data?.runs || [];
951
+ if (folder)
952
+ runs = runs.filter((r) => r.folder === folder);
953
+ return limit > 0 ? runs.slice(0, limit) : runs;
954
+ }
955
+ /** Read a single stored http run (full request/response detail) by id. */
956
+ export function loadHttpRun(runId) {
957
+ return safeReadJsonSync(getHttpRunFile(runId), `Http run ${runId}`);
958
+ }
959
+ /**
960
+ * Persist an executed request: write its full file, prepend a summary to the
961
+ * index, cap at MAX_HTTP_RUNS, prune per-run files that fell out.
962
+ */
963
+ export function saveHttpRun(run) {
964
+ ensureHttpRunsDir();
965
+ try {
966
+ atomicWriteJsonSync(getHttpRunFile(run.runId), run);
967
+ const existing = loadHttpRunSummaries(0).filter((r) => r.runId !== run.runId);
968
+ const runs = [summarizeHttpRun(run), ...existing];
969
+ const kept = runs.slice(0, MAX_HTTP_RUNS);
970
+ const dropped = runs.slice(MAX_HTTP_RUNS);
971
+ atomicWriteJsonSync(HTTP_RUNS_INDEX_FILE, {
972
+ runs: kept,
973
+ savedAt: Date.now(),
974
+ version: '1.0.0',
975
+ });
976
+ for (const d of dropped) {
977
+ try {
978
+ const f = getHttpRunFile(d.runId);
979
+ if (fs.existsSync(f))
980
+ fs.unlinkSync(f);
981
+ }
982
+ catch {
983
+ // best-effort prune
984
+ }
985
+ }
986
+ }
987
+ catch (err) {
988
+ log.error(' Failed to save http run:', err);
989
+ }
990
+ }
@@ -139,7 +139,10 @@ router.post('/cdp-raw', (req, res) => relay(res, 'cdp_raw', { ...target(req), me
139
139
  // ── tabs (open / close / activate) ──
140
140
  router.post('/tab/open', (req, res) => relay(res, 'tab_open', { url: req.body?.url, active: req.body?.active }));
141
141
  router.post('/tab/close', (req, res) => relay(res, 'tab_close', target(req)));
142
- router.post('/tab/activate', (req, res) => relay(res, 'tab_activate', target(req)));
142
+ // Activating a tab makes it the ACTIVE tab of its window (so full-viewport screenshots
143
+ // work) WITHOUT stealing the user's OS focus. Pass focusWindow:true to also raise the
144
+ // window to the foreground (interrupts whatever the user is doing) — opt-in only.
145
+ router.post('/tab/activate', (req, res) => relay(res, 'tab_activate', { ...target(req), focusWindow: req.body?.focusWindow }));
143
146
  // ── fallback: drive a SEPARATE Chrome via puppeteer-core over the debug port ──
144
147
  // For a throwaway --user-data-dir profile or Chrome for Testing (not your live
145
148
  // session). Requires Chrome launched with --remote-debugging-port. See cdp-service.ts.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * HTTP Request Tests Routes (IntelliJ-style .http files)
3
+ *
4
+ * REST API for the "http" building: scan a folder for .http files and fire
5
+ * individual requests. Runs are synchronous — a single HTTP request resolves
6
+ * quickly (60s server-side timeout), so no runId/WebSocket plumbing is needed.
7
+ */
8
+ import { Router } from 'express';
9
+ import { httpRequestsService } from '../services/index.js';
10
+ import { createLogger, generateId } from '../utils/index.js';
11
+ import { saveHttpRun, loadHttpRun, loadHttpRunSummaries } from '../data/index.js';
12
+ const log = createLogger('HttpRequestsRoute');
13
+ const router = Router();
14
+ // Store for broadcasting via WebSocket (wired from the websocket handler).
15
+ let broadcastFn = null;
16
+ export function setBroadcast(fn) {
17
+ broadcastFn = fn;
18
+ }
19
+ function broadcast(message) {
20
+ broadcastFn?.(message);
21
+ }
22
+ /**
23
+ * POST /api/http-requests/scan - Parse every .http/.rest file under a folder.
24
+ * Body: { path: string }
25
+ */
26
+ router.post('/scan', (req, res) => {
27
+ const { path: targetPath } = req.body;
28
+ if (!targetPath) {
29
+ res.status(400).json({ error: 'Missing required field: path' });
30
+ return;
31
+ }
32
+ if (!httpRequestsService.isSafeFolder(targetPath)) {
33
+ res.status(400).json({ error: 'Refusing to scan folders outside your home directory.' });
34
+ return;
35
+ }
36
+ res.json(httpRequestsService.scanFolder(targetPath));
37
+ });
38
+ /**
39
+ * POST /api/http-requests/run - Execute one request from a .http file.
40
+ * Body: { path, relFile, requestIndex, env? } — responds with HttpRunResult
41
+ * once the request completes (or fails / times out).
42
+ */
43
+ router.post('/run', async (req, res) => {
44
+ const { path: targetPath, relFile, requestIndex, env, agentId } = req.body;
45
+ if (!targetPath || !relFile || typeof requestIndex !== 'number') {
46
+ res.status(400).json({ error: 'Missing required fields: path, relFile, requestIndex' });
47
+ return;
48
+ }
49
+ if (!httpRequestsService.isSafeFolder(targetPath)) {
50
+ res.status(400).json({ error: 'Refusing to run requests outside your home directory.' });
51
+ return;
52
+ }
53
+ const startAgentId = typeof agentId === 'string' && agentId ? agentId : undefined;
54
+ const runId = generateId();
55
+ try {
56
+ // Announce the run when an agent fired it, so its terminal shows a live
57
+ // inline card immediately (spinner → result), like test runs.
58
+ if (startAgentId) {
59
+ const item = httpRequestsService.peekRequest(targetPath, relFile, requestIndex);
60
+ broadcast({
61
+ type: 'http_run_started',
62
+ payload: {
63
+ runId,
64
+ agentId: startAgentId,
65
+ folder: targetPath,
66
+ relFile,
67
+ requestIndex,
68
+ requestName: item?.name || `${relFile}#${requestIndex}`,
69
+ method: item?.method || 'GET',
70
+ url: item?.url || '',
71
+ env,
72
+ },
73
+ });
74
+ }
75
+ const result = await httpRequestsService.executeRequest(targetPath, relFile, requestIndex, env);
76
+ log.log(`${result.request.method} ${result.request.url} → ` +
77
+ (result.ok ? `${result.status} in ${result.timeMs}ms` : `error: ${result.error}`));
78
+ // Persist to history (best-effort — the response never waits on failures
79
+ // beyond the synchronous write).
80
+ const stored = {
81
+ runId,
82
+ folder: targetPath,
83
+ relFile,
84
+ requestIndex,
85
+ requestName: result.requestName || `${result.request.method} ${result.request.url}`,
86
+ method: result.request.method,
87
+ url: result.request.url,
88
+ env,
89
+ ok: result.ok,
90
+ status: result.status,
91
+ timeMs: result.timeMs,
92
+ sizeBytes: result.sizeBytes,
93
+ error: result.error,
94
+ finishedAt: Date.now(),
95
+ result,
96
+ };
97
+ saveHttpRun(stored);
98
+ if (startAgentId) {
99
+ broadcast({
100
+ type: 'http_run_completed',
101
+ payload: { runId, agentId: startAgentId, result },
102
+ });
103
+ }
104
+ res.json({ ...result, runId });
105
+ }
106
+ catch (err) {
107
+ // Close the inline card on hard failures too (parse error, missing file).
108
+ if (startAgentId) {
109
+ broadcast({
110
+ type: 'http_run_completed',
111
+ payload: {
112
+ runId,
113
+ agentId: startAgentId,
114
+ result: {
115
+ ok: false,
116
+ request: { method: 'GET', url: '', headers: [] },
117
+ timeMs: 0,
118
+ error: err?.message || 'Failed to run request',
119
+ },
120
+ },
121
+ });
122
+ }
123
+ res.status(400).json({ error: err?.message || 'Failed to run request' });
124
+ }
125
+ });
126
+ /**
127
+ * GET /api/http-requests/history - Recent executed requests (newest first).
128
+ * Query: ?folder=<abs path> scopes to one building's folder; ?limit=N (default
129
+ * 50, max 200). Summaries only — fetch /runs/:runId for the full detail.
130
+ */
131
+ router.get('/history', (req, res) => {
132
+ const raw = parseInt(req.query.limit, 10);
133
+ const limit = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 200) : 50;
134
+ const folder = typeof req.query.folder === 'string' && req.query.folder ? req.query.folder : undefined;
135
+ res.json({ runs: loadHttpRunSummaries(limit, folder) });
136
+ });
137
+ /**
138
+ * GET /api/http-requests/runs/:runId - Full stored run (request + response).
139
+ */
140
+ router.get('/runs/:runId', (req, res) => {
141
+ const run = loadHttpRun(req.params.runId);
142
+ if (!run) {
143
+ res.status(404).json({ error: 'Run not found' });
144
+ return;
145
+ }
146
+ res.json(run);
147
+ });
148
+ export default router;
@@ -11,6 +11,7 @@ import agentPromptRouter from './agent-prompt.js';
11
11
  import notificationsRouter, { setBroadcast as setNotificationBroadcast } from './notifications.js';
12
12
  import execRouter, { setBroadcast as setExecBroadcast } from './exec.js';
13
13
  import testsRouter, { setBroadcast as setTestsBroadcast } from './tests.js';
14
+ import httpRequestsRouter, { setBroadcast as setHttpRequestsBroadcast } from './http-requests.js';
14
15
  import focusAgentRouter, { setBroadcast as setFocusAgentBroadcast } from './focus-agent.js';
15
16
  import customModelsRouter from './custom-models.js';
16
17
  import configRouter from './config.js';
@@ -44,6 +45,7 @@ router.use('/folders', foldersRouter);
44
45
  router.use('/notify', notificationsRouter);
45
46
  router.use('/exec', execRouter);
46
47
  router.use('/tests', testsRouter);
48
+ router.use('/http-requests', httpRequestsRouter);
47
49
  router.use('/focus-agent', focusAgentRouter);
48
50
  router.use('/custom-models', customModelsRouter);
49
51
  router.use('/tts', ttsRouter);
@@ -83,5 +85,5 @@ router.use('/', permissionsRouter);
83
85
  // Agent prompt routes — root-level: /api/agent-prompt and /api/agent-prompt/:id/respond
84
86
  router.use('/', agentPromptRouter);
85
87
  // Export the broadcast setters for WebSocket handler to use
86
- export { setNotificationBroadcast, setExecBroadcast, setTestsBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast, setSkillsBroadcast };
88
+ export { setNotificationBroadcast, setExecBroadcast, setTestsBroadcast, setHttpRequestsBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast, setSkillsBroadcast };
87
89
  export default router;
@@ -3,6 +3,8 @@
3
3
  * Endpoints for inspecting / updating the running Tide Commander install.
4
4
  */
5
5
  import { Router } from 'express';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
6
8
  import { createLogger } from '../utils/logger.js';
7
9
  import { fetchLatestNpmVersion, getVersionRelation } from '../../shared/version.js';
8
10
  import { getInstallInfo, isAutoUpdateSupported, runNpmGlobalUpdate, schedulePostUpdateRestart, } from '../services/self-update-service.js';
@@ -45,6 +47,34 @@ router.get('/install-info', async (_req, res) => {
45
47
  res.status(500).json({ error: message });
46
48
  }
47
49
  });
50
+ /**
51
+ * GET /api/system/changelog
52
+ *
53
+ * Serves the bundled CHANGELOG.md (Keep a Changelog markdown) so the UI can
54
+ * render release notes locally — no GitHub API, no rate limits, works offline
55
+ * and always matches the installed version.
56
+ */
57
+ router.get('/changelog', (_req, res) => {
58
+ try {
59
+ const installRoot = getInstallInfo().installRoot;
60
+ if (!installRoot) {
61
+ res.status(404).json({ error: 'Could not locate install root.' });
62
+ return;
63
+ }
64
+ const changelogPath = path.join(installRoot, 'CHANGELOG.md');
65
+ if (!fs.existsSync(changelogPath)) {
66
+ res.status(404).json({ error: 'CHANGELOG.md not found.' });
67
+ return;
68
+ }
69
+ const content = fs.readFileSync(changelogPath, 'utf8');
70
+ res.type('text/markdown').send(content);
71
+ }
72
+ catch (err) {
73
+ const message = err.message;
74
+ log.error(`Failed to read changelog: ${message}`);
75
+ res.status(500).json({ error: message });
76
+ }
77
+ });
48
78
  /**
49
79
  * POST /api/system/self-update
50
80
  *
@@ -1088,7 +1088,7 @@ export async function handleBuildingSync(newBuildings, _broadcast) {
1088
1088
  // Validation & CRUD (Phase 1 - REST API support)
1089
1089
  // ============================================================================
1090
1090
  const VALID_TYPES = new Set([
1091
- 'server', 'link', 'database', 'docker', 'monitor', 'folder', 'boss', 'terminal', 'tests',
1091
+ 'server', 'link', 'database', 'docker', 'monitor', 'folder', 'boss', 'terminal', 'tests', 'http',
1092
1092
  ]);
1093
1093
  const VALID_STYLES = new Set([
1094
1094
  'server-rack', 'tower', 'dome', 'pyramid', 'desktop',
@@ -1113,6 +1113,7 @@ const DEFAULT_STYLE_BY_TYPE = {
1113
1113
  boss: 'command-center',
1114
1114
  terminal: 'desktop',
1115
1115
  tests: 'dome',
1116
+ http: 'satellite',
1116
1117
  };
1117
1118
  function generateBuildingId(name) {
1118
1119
  const slug = name
@@ -1203,6 +1204,9 @@ export function validateBuilding(input) {
1203
1204
  if (input.type === 'tests' && !input.folderPath) {
1204
1205
  errors.push('folderPath is required for tests buildings (the folder whose tests it browses)');
1205
1206
  }
1207
+ if (input.type === 'http' && !input.folderPath) {
1208
+ errors.push('folderPath is required for http buildings (the folder containing the .http request files)');
1209
+ }
1206
1210
  if (input.type === 'link' && (!input.urls || input.urls.length === 0)) {
1207
1211
  errors.push('urls is required (non-empty array) for link buildings');
1208
1212
  }