tide-commander 1.94.0 → 1.96.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 (52) hide show
  1. package/dist/assets/{BossLogsModal-b4O2O00j.js → BossLogsModal-BFy6Rxdn.js} +1 -1
  2. package/dist/assets/{BossSpawnModal--Cn40tMQ.js → BossSpawnModal-KQyJYj_B.js} +1 -1
  3. package/dist/assets/{ControlsModal-SNJ95Qn9.js → ControlsModal-W_ghuG_Q.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-DOkOsLaK.js → DockerLogsModal-valTcuRy.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-BKLJKK3B.js → EmbeddedEditor-CIsFWo6A.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-B8ao6401.js → GmailOAuthSetup-AcW1zAlk.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-By-gup6S.js → GoogleOAuthSetup-C9uZ46Oe.js} +1 -1
  8. package/dist/assets/{IframeModal-KzrFyAG0.js → IframeModal-CmWv7GXl.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CXMEldir.js → IntegrationsPanel-CNvzosgx.js} +2 -2
  10. package/dist/assets/{LogViewerModal-C70SfK7U.js → LogViewerModal-imTE-619.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CbZtFYh9.js → MonitoringModal-DLxBY738.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-C8mo6Tqu.js → PM2LogsModal-S4n0D11V.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BUB-mruP.js → RestoreArchivedAreaModal-B32sqCxN.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-zinDgDAK.js → Scene2DCanvas-bEu0UPUm.js} +1 -1
  15. package/dist/assets/{SceneManager-CAxi1UHT.js → SceneManager-Ce3X0awy.js} +1 -1
  16. package/dist/assets/{SkillsPanel-Bh_J9suc.js → SkillsPanel-Dj22nDU8.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-DZSpM5ew.js → SlackMultiInstanceSetup-J9NdRY0U.js} +1 -1
  18. package/dist/assets/{SpawnModal-BrcKWjDs.js → SpawnModal-F9nspdWd.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-CBqmszjc.js → SubordinateAssignmentModal-DT31CkPr.js} +1 -1
  20. package/dist/assets/TriggerManagerPanel-CVJ9ozHz.js +9 -0
  21. package/dist/assets/{WorkflowEditorPanel-DE9448gZ.js → WorkflowEditorPanel-BYRi0cU9.js} +1 -1
  22. package/dist/assets/{index-DuDcnBNX.js → index-B9G4hrFR.js} +1 -1
  23. package/dist/assets/{index-DPvgEcBT.js → index-BPy8NE5B.js} +3 -3
  24. package/dist/assets/{index-Cbk6B5BY.js → index-BmjrPptb.js} +1 -1
  25. package/dist/assets/{index-DelPL0HI.js → index-ByfAbkqv.js} +1 -1
  26. package/dist/assets/{index-B4VnKOHB.js → index-C5wC_Xis.js} +1 -1
  27. package/dist/assets/{index-Dk2THz0j.js → index-CLr8DpLX.js} +2 -2
  28. package/dist/assets/{index-BkZ422b_.js → index-DJm7oMXk.js} +1 -1
  29. package/dist/assets/{index-CpLZo6IM.js → index-DkjWoHhy.js} +1 -1
  30. package/dist/assets/{index-C7EieXis.js → index-DqZhHROd.js} +1 -1
  31. package/dist/assets/{main-CteALl_G.js → main-9uTEp9Lr.js} +4 -4
  32. package/dist/assets/{web-CGYGq80f.js → web-CLuvtqCm.js} +1 -1
  33. package/dist/assets/{web-BZa7rL0p.js → web-D-a3phls.js} +1 -1
  34. package/dist/assets/{web-DtevCxzm.js → web-DM2bFWBN.js} +1 -1
  35. package/dist/index.html +1 -1
  36. package/dist/src/packages/server/app.js +14 -0
  37. package/dist/src/packages/server/data/builtin-skills/bitbucket-pr-review.js +431 -0
  38. package/dist/src/packages/server/data/builtin-skills/bitbucket-reviewer.js +191 -0
  39. package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
  40. package/dist/src/packages/server/data/builtin-skills/index.js +4 -0
  41. package/dist/src/packages/server/data/builtin-skills/trigger-designer.js +246 -71
  42. package/dist/src/packages/server/routes/bitbucket-author-loop.js +54 -0
  43. package/dist/src/packages/server/routes/buildings.js +298 -0
  44. package/dist/src/packages/server/routes/index.js +3 -1
  45. package/dist/src/packages/server/routes/trigger-routes.js +68 -15
  46. package/dist/src/packages/server/routes/webhook-dedupe.js +58 -0
  47. package/dist/src/packages/server/routes/webhook-signatures.js +67 -0
  48. package/dist/src/packages/server/services/building-service.js +400 -85
  49. package/dist/src/packages/server/services/trigger-service.js +5 -3
  50. package/dist/src/packages/server/websocket/handler.js +2 -4
  51. package/package.json +1 -1
  52. package/dist/assets/TriggerManagerPanel-BD7VnHYc.js +0 -9
@@ -1 +1 @@
1
- import{ck as a}from"./main-CteALl_G.js";import{ImpactStyle as i,NotificationType as r}from"./index-Dk2THz0j.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{ck as a}from"./main-9uTEp9Lr.js";import{ImpactStyle as i,NotificationType as r}from"./index-CLr8DpLX.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{ck as t}from"./main-CteALl_G.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{ck as t}from"./main-9uTEp9Lr.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{ck as s}from"./main-CteALl_G.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{ck as s}from"./main-9uTEp9Lr.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,7 +22,7 @@
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-CteALl_G.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-9uTEp9Lr.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
28
  <link rel="stylesheet" crossorigin href="/assets/main-BfT_95fk.css">
@@ -53,6 +53,20 @@ export function createApp() {
53
53
  const app = express();
54
54
  // Middleware
55
55
  app.use(cors());
56
+ // Webhook routes need the raw request bytes for HMAC signature verification.
57
+ // Bitbucket and GitHub compute the signature over the exact bytes they send,
58
+ // so re-serializing via JSON.stringify(req.body) can produce a different
59
+ // digest (key ordering / whitespace) and falsely reject valid deliveries.
60
+ // The path-scoped JSON parser below runs first; it parses the body AND
61
+ // stashes the raw buffer on req.rawBody. Once req._body is set, the global
62
+ // express.json that follows is a no-op for these requests, so the raw
63
+ // buffer is only retained for webhook deliveries — not every JSON request.
64
+ app.use('/api/triggers/webhook', express.json({
65
+ limit: '50mb',
66
+ verify: (req, _res, buf) => {
67
+ req.rawBody = buf;
68
+ },
69
+ }));
56
70
  app.use(express.json({ limit: '50mb' })); // Increased for audio uploads (STT)
57
71
  // Request logging & timing
58
72
  app.use((req, res, next) => {
@@ -0,0 +1,431 @@
1
+ export const bitbucketPRReview = {
2
+ slug: 'bitbucket-pr-review',
3
+ name: 'Bitbucket PR Review',
4
+ description: 'Read pull request diffs, post review comments (inline or summary), approve or request-changes on Bitbucket Cloud PRs using API token authentication. Use this skill when an automated reviewer agent needs to evaluate a PR and record a verdict.',
5
+ allowedTools: ['Bash(curl:*)', 'Read', 'Grep', 'Glob'],
6
+ content: `# Bitbucket Pull Request Reviewer
7
+
8
+ Read PR diffs, post review comments, and set verdict (approve / request-changes / clear) on Bitbucket Cloud pull requests via the REST API 2.0.
9
+
10
+ > **Note:** Bitbucket deprecated App Passwords in September 2025 (disabled June 9, 2026). This skill uses API tokens with scopes.
11
+
12
+ ## Required Secrets
13
+
14
+ | Secret Key | Description |
15
+ |------------|-------------|
16
+ | \`BITBUCKET_EMAIL\` | Atlassian account email for the reviewer identity |
17
+ | \`BITBUCKET_TOKEN\` | Bitbucket API token with PR review scopes |
18
+
19
+ **Required scopes:**
20
+ - \`read:repository:bitbucket\` — fetch diff
21
+ - \`read:pullrequest:bitbucket\` — read PR + comments + participants
22
+ - \`write:pullrequest:bitbucket\` — comments, approve/unapprove, request-changes/unrequest-changes
23
+
24
+ Do **not** request \`write:repository:bitbucket\` — a reviewer must never push.
25
+
26
+ **Authentication:** HTTP Basic, built from the secret placeholders:
27
+ \`\`\`bash
28
+ curl -s -u "{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}" ...
29
+ \`\`\`
30
+
31
+ ---
32
+
33
+ ## CRITICAL: All API Calls Must Use Streaming Exec
34
+
35
+ Secret placeholders \`{{BITBUCKET_EMAIL}}\` / \`{{BITBUCKET_TOKEN}}\` are **only** interpolated when commands run through the Streaming Exec API (\`/api/exec\`). Direct Bash curl calls will send the raw \`{{...}}\` placeholders and 401.
36
+
37
+ **Wrap every curl command:**
38
+ \`\`\`bash
39
+ curl -s -X POST http://localhost:5174/api/exec \\
40
+ -H "Content-Type: application/json" \\
41
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/diff"}'
42
+ \`\`\`
43
+
44
+ If you get a 401, the most likely cause is calling curl directly instead of through \`/api/exec\`.
45
+
46
+ ---
47
+
48
+ ## Common Variables
49
+
50
+ | Variable | Description | Example |
51
+ |----------|-------------|---------|
52
+ | \`WORKSPACE\` | Bitbucket workspace slug | \`mycompany\` |
53
+ | \`REPO_SLUG\` | Repository slug | \`wind\` |
54
+ | \`PR_ID\` | Pull request id | \`42\` |
55
+
56
+ These typically arrive in the trigger payload (extracted from \`repository.full_name\` and \`pullrequest.id\` by the webhook trigger).
57
+
58
+ \`\`\`bash
59
+ # Split workspace/repo from full_name="myws/wind"
60
+ WORKSPACE=$(echo "$REPO_FULL_NAME" | cut -d/ -f1)
61
+ REPO_SLUG=$(echo "$REPO_FULL_NAME" | cut -d/ -f2)
62
+ \`\`\`
63
+
64
+ Base URL: \`https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG\`
65
+
66
+ ---
67
+
68
+ ## Action: fetch_pr_diff
69
+
70
+ Retrieve the unified diff for a pull request.
71
+
72
+ **Endpoint:** \`GET /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diff\`
73
+
74
+ **Input Schema:**
75
+ \`\`\`json
76
+ {
77
+ "type": "object",
78
+ "required": ["workspace", "repo_slug", "pull_request_id"],
79
+ "properties": {
80
+ "workspace": { "type": "string", "description": "Bitbucket workspace slug" },
81
+ "repo_slug": { "type": "string", "description": "Repository slug" },
82
+ "pull_request_id": { "type": "integer", "description": "PR id" },
83
+ "max_bytes": { "type": "integer", "default": 204800, "description": "Truncate diff at this many bytes (default 200 KB)" }
84
+ }
85
+ }
86
+ \`\`\`
87
+
88
+ **Call (200 KB cap, returns JSON \`{ diff, truncated, byte_count }\`):**
89
+ \`\`\`bash
90
+ curl -s -X POST http://localhost:5174/api/exec \\
91
+ -H "Content-Type: application/json" \\
92
+ -d '{"agentId":"YOUR_AGENT_ID","command":"set -euo pipefail; MAX=\${MAX_BYTES:-204800}; RAW=$(curl -s -H \\"Accept: text/plain\\" -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/diff\\"); LEN=\${#RAW}; if [ \\"$LEN\\" -gt \\"$MAX\\" ]; then DIFF=\${RAW:0:$MAX}; TRUNC=true; else DIFF=$RAW; TRUNC=false; fi; jq -nc --arg d \\"$DIFF\\" --argjson t $TRUNC --argjson l $LEN \\"{diff:\\$d, truncated:\\$t, byte_count:\\$l}\\""}'
93
+ \`\`\`
94
+
95
+ **Output (JSON):**
96
+ \`\`\`json
97
+ { "diff": "<unified diff text, truncated to max_bytes>", "truncated": false, "byte_count": 12345 }
98
+ \`\`\`
99
+
100
+ If \`truncated:true\`, include a note in the summary comment that the review was based on a partial diff and recommend splitting the PR.
101
+
102
+ ---
103
+
104
+ ## Action: fetch_pr_diffstat
105
+
106
+ Retrieve per-file change statistics (additions/removals, file paths). Use as a pre-flight gate before \`fetch_pr_diff\` to skip oversized PRs early.
107
+
108
+ **Endpoint:** \`GET /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat\`
109
+
110
+ **Input Schema:**
111
+ \`\`\`json
112
+ {
113
+ "type": "object",
114
+ "required": ["workspace", "repo_slug", "pull_request_id"],
115
+ "properties": {
116
+ "workspace": { "type": "string" },
117
+ "repo_slug": { "type": "string" },
118
+ "pull_request_id": { "type": "integer" },
119
+ "pagelen": { "type": "integer", "default": 100, "description": "Page size (max 100)" }
120
+ }
121
+ }
122
+ \`\`\`
123
+
124
+ **Call:**
125
+ \`\`\`bash
126
+ curl -s -X POST http://localhost:5174/api/exec \\
127
+ -H "Content-Type: application/json" \\
128
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/diffstat?pagelen=100\\""}'
129
+ \`\`\`
130
+
131
+ **Output (Bitbucket paginated response):**
132
+ \`\`\`json
133
+ {
134
+ "values": [
135
+ { "type": "diffstat", "status": "modified", "lines_added": 12, "lines_removed": 3,
136
+ "old": { "path": "src/foo.ts" }, "new": { "path": "src/foo.ts" } }
137
+ ],
138
+ "page": 1, "pagelen": 100, "size": 1, "next": null
139
+ }
140
+ \`\`\`
141
+
142
+ If the result spans multiple pages, follow \`next\` URLs to fetch the rest.
143
+
144
+ ---
145
+
146
+ ## Action: post_inline_comment
147
+
148
+ Post a comment anchored to a specific line in a file in the PR diff.
149
+
150
+ **Endpoint:** \`POST /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments\`
151
+
152
+ **Input Schema:**
153
+ \`\`\`json
154
+ {
155
+ "type": "object",
156
+ "required": ["workspace", "repo_slug", "pull_request_id", "path", "line", "side", "text"],
157
+ "properties": {
158
+ "workspace": { "type": "string" },
159
+ "repo_slug": { "type": "string" },
160
+ "pull_request_id": { "type": "integer" },
161
+ "path": { "type": "string", "description": "File path inside the repo (e.g. src/foo.ts)" },
162
+ "line": { "type": "integer", "description": "Line number in the file (1-indexed)" },
163
+ "side": { "type": "string", "enum": ["to", "from"], "description": "'to' = line in destination/new file; 'from' = line in source/old file" },
164
+ "text": { "type": "string", "description": "Comment body (markdown)" }
165
+ }
166
+ }
167
+ \`\`\`
168
+
169
+ **Body shape (the \`inline\` object distinguishes inline from summary comments):**
170
+ \`\`\`json
171
+ {
172
+ "content": { "raw": "<text>" },
173
+ "inline": { "path": "<path>", "to": <line> }
174
+ }
175
+ \`\`\`
176
+ Use \`"from"\` instead of \`"to"\` when commenting on a removed/old line.
177
+
178
+ **Call:**
179
+ \`\`\`bash
180
+ curl -s -X POST http://localhost:5174/api/exec \\
181
+ -H "Content-Type: application/json" \\
182
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -X POST -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" -H \\"Content-Type: application/json\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/comments\\" -d \\"{\\\\\\"content\\\\\\":{\\\\\\"raw\\\\\\":\\\\\\"$TEXT\\\\\\"},\\\\\\"inline\\\\\\":{\\\\\\"path\\\\\\":\\\\\\"$PATH\\\\\\",\\\\\\"$SIDE\\\\\\":$LINE}}\\""}'
183
+ \`\`\`
184
+
185
+ **Output:** Bitbucket comment object with \`id\`, \`content.raw\`, \`inline\`, \`user\`, \`created_on\`.
186
+
187
+ ---
188
+
189
+ ## Action: post_summary_comment
190
+
191
+ Post a top-level (non-inline) comment summarizing the review.
192
+
193
+ **Endpoint:** \`POST /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments\` (same endpoint, body omits \`inline\`)
194
+
195
+ **Input Schema:**
196
+ \`\`\`json
197
+ {
198
+ "type": "object",
199
+ "required": ["workspace", "repo_slug", "pull_request_id", "text"],
200
+ "properties": {
201
+ "workspace": { "type": "string" },
202
+ "repo_slug": { "type": "string" },
203
+ "pull_request_id": { "type": "integer" },
204
+ "text": { "type": "string", "description": "Summary comment body (markdown)" }
205
+ }
206
+ }
207
+ \`\`\`
208
+
209
+ **Body:**
210
+ \`\`\`json
211
+ { "content": { "raw": "## Verdict\\n\\nLGTM — approving.\\n..." } }
212
+ \`\`\`
213
+
214
+ **Call:**
215
+ \`\`\`bash
216
+ curl -s -X POST http://localhost:5174/api/exec \\
217
+ -H "Content-Type: application/json" \\
218
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -X POST -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" -H \\"Content-Type: application/json\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/comments\\" -d \\"{\\\\\\"content\\\\\\":{\\\\\\"raw\\\\\\":\\\\\\"$TEXT\\\\\\"}}\\""}'
219
+ \`\`\`
220
+
221
+ Tip: tag the verdict so re-reviews can find it via \`list_existing_comments\` (e.g. start the body with \`<!-- tide-commander-verdict -->\`).
222
+
223
+ ---
224
+
225
+ ## Action: approve
226
+
227
+ Set the calling user's participant state to \`approved\`.
228
+
229
+ **Endpoint:** \`POST /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/approve\`
230
+
231
+ **Input Schema:**
232
+ \`\`\`json
233
+ {
234
+ "type": "object",
235
+ "required": ["workspace", "repo_slug", "pull_request_id"],
236
+ "properties": {
237
+ "workspace": { "type": "string" },
238
+ "repo_slug": { "type": "string" },
239
+ "pull_request_id": { "type": "integer" }
240
+ }
241
+ }
242
+ \`\`\`
243
+
244
+ **Call:**
245
+ \`\`\`bash
246
+ curl -s -X POST http://localhost:5174/api/exec \\
247
+ -H "Content-Type: application/json" \\
248
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -X POST -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/approve\\""}'
249
+ \`\`\`
250
+
251
+ **Output:** participant object with \`approved: true\`, \`state: "approved"\`.
252
+
253
+ If a prior pass called \`request_changes\`, also call \`unrequest_changes\` so the participant state isn't ambiguous.
254
+
255
+ ---
256
+
257
+ ## Action: unapprove
258
+
259
+ Clear the calling user's \`approved\` state.
260
+
261
+ **Endpoint:** \`DELETE /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/approve\`
262
+
263
+ **Input Schema:** identical to \`approve\`.
264
+
265
+ **Call:**
266
+ \`\`\`bash
267
+ curl -s -X POST http://localhost:5174/api/exec \\
268
+ -H "Content-Type: application/json" \\
269
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -X DELETE -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/approve\\""}'
270
+ \`\`\`
271
+
272
+ **Output:** \`204 No Content\` on success.
273
+
274
+ ---
275
+
276
+ ## Action: request_changes
277
+
278
+ Set the calling user's participant state to \`changes_requested\` (native Bitbucket Cloud endpoint — not emulated).
279
+
280
+ **Endpoint:** \`POST /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/request-changes\`
281
+
282
+ **Input Schema:**
283
+ \`\`\`json
284
+ {
285
+ "type": "object",
286
+ "required": ["workspace", "repo_slug", "pull_request_id"],
287
+ "properties": {
288
+ "workspace": { "type": "string" },
289
+ "repo_slug": { "type": "string" },
290
+ "pull_request_id": { "type": "integer" }
291
+ }
292
+ }
293
+ \`\`\`
294
+
295
+ **Call:**
296
+ \`\`\`bash
297
+ curl -s -X POST http://localhost:5174/api/exec \\
298
+ -H "Content-Type: application/json" \\
299
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -X POST -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/request-changes\\""}'
300
+ \`\`\`
301
+
302
+ **Output:** participant object with \`state: "changes_requested"\`.
303
+
304
+ If a prior pass called \`approve\`, also call \`unapprove\` so the participant state isn't ambiguous.
305
+
306
+ ---
307
+
308
+ ## Action: unrequest_changes
309
+
310
+ Clear the calling user's \`changes_requested\` state.
311
+
312
+ **Endpoint:** \`DELETE /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/request-changes\`
313
+
314
+ **Input Schema:** identical to \`request_changes\`.
315
+
316
+ **Call:**
317
+ \`\`\`bash
318
+ curl -s -X POST http://localhost:5174/api/exec \\
319
+ -H "Content-Type: application/json" \\
320
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -X DELETE -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/request-changes\\""}'
321
+ \`\`\`
322
+
323
+ **Output:** \`204 No Content\` on success.
324
+
325
+ ---
326
+
327
+ ## Action: list_existing_comments
328
+
329
+ List all comments on a PR. Use to detect prior reviewer verdict comments (idempotency for \`pullrequest:updated\` retries) and to avoid duplicate inline comments.
330
+
331
+ **Endpoint:** \`GET /2.0/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments?pagelen=100\`
332
+
333
+ **Input Schema:**
334
+ \`\`\`json
335
+ {
336
+ "type": "object",
337
+ "required": ["workspace", "repo_slug", "pull_request_id"],
338
+ "properties": {
339
+ "workspace": { "type": "string" },
340
+ "repo_slug": { "type": "string" },
341
+ "pull_request_id": { "type": "integer" },
342
+ "pagelen": { "type": "integer", "default": 100, "description": "Page size (max 100)" },
343
+ "follow_pagination": { "type": "boolean", "default": true, "description": "If true, follow 'next' URLs and concatenate all pages" }
344
+ }
345
+ }
346
+ \`\`\`
347
+
348
+ **Call (single page):**
349
+ \`\`\`bash
350
+ curl -s -X POST http://localhost:5174/api/exec \\
351
+ -H "Content-Type: application/json" \\
352
+ -d '{"agentId":"YOUR_AGENT_ID","command":"curl -s -u \\"{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}\\" \\"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/comments?pagelen=100\\""}'
353
+ \`\`\`
354
+
355
+ **Pagination loop (concatenate all pages into one JSON array):**
356
+ \`\`\`bash
357
+ URL="https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO_SLUG/pullrequests/$PR_ID/comments?pagelen=100"
358
+ ALL="[]"
359
+ while [ -n "$URL" ] && [ "$URL" != "null" ]; do
360
+ PAGE=$(curl -s -u "{{BITBUCKET_EMAIL}}:{{BITBUCKET_TOKEN}}" "$URL")
361
+ ALL=$(jq -c --argjson page "$PAGE" '. + $page.values' <<<"$ALL")
362
+ URL=$(jq -r '.next // empty' <<<"$PAGE")
363
+ done
364
+ echo "$ALL"
365
+ \`\`\`
366
+
367
+ **Output:** array of comment objects with \`id\`, \`content.raw\`, \`inline\` (for inline comments), \`user.account_id\`, \`created_on\`, \`updated_on\`, \`deleted\`.
368
+
369
+ To find a prior verdict comment, filter for \`content.raw\` starting with the \`<!-- tide-commander-verdict -->\` marker.
370
+
371
+ ---
372
+
373
+ ## Verdict Workflow
374
+
375
+ Symmetric, two-state verdict — these always run together so the participant state stays consistent:
376
+
377
+ | Verdict | Calls |
378
+ |---------|-------|
379
+ | Approve | \`post_summary_comment\` + \`approve\` + (if prior pass requested changes) \`unrequest_changes\` |
380
+ | Request changes | \`post_summary_comment\` + \`request_changes\` + (if prior pass approved) \`unapprove\` |
381
+
382
+ On a \`pullrequest:updated\` event, first call \`list_existing_comments\` and look for the \`<!-- tide-commander-verdict -->\` marker to decide whether to update an existing verdict (delete + repost) vs add a new one.
383
+
384
+ ---
385
+
386
+ ## Idempotency Rules
387
+
388
+ 1. The webhook payload includes \`X-Request-UUID\`. Persist the last-handled UUID per PR and skip re-processing the same delivery.
389
+ 2. Before posting a summary comment, call \`list_existing_comments\` and check for the \`<!-- tide-commander-verdict -->\` marker. If found, delete the prior comment first (\`DELETE …/comments/{comment_id}\`) before posting the new one.
390
+ 3. Inline comments are harder to dedupe — match on \`{path, line, side}\` and skip if a comment from the reviewer account already exists at that anchor.
391
+
392
+ ---
393
+
394
+ ## Error Handling
395
+
396
+ | HTTP | Meaning | Action |
397
+ |------|---------|--------|
398
+ | 401 | Unauthorized | Verify \`BITBUCKET_EMAIL\` / \`BITBUCKET_TOKEN\` are set; confirm the curl ran via \`/api/exec\` |
399
+ | 403 | Forbidden | Token is missing \`write:pullrequest:bitbucket\` (or \`read:pullrequest:bitbucket\` for reads) |
400
+ | 404 | Not Found | Wrong workspace/repo/PR id — re-extract from the trigger payload |
401
+ | 400 | Bad Request | JSON body malformed (commonly an unescaped quote in the comment body) |
402
+ | 429 | Rate limited | Bitbucket rate-limits per-user; back off and retry the failed action only |
403
+
404
+ ---
405
+
406
+ ## Safety Rules
407
+
408
+ 1. **Never push code, never merge, never decline** — those are not reviewer responsibilities. Required scopes intentionally exclude \`write:repository:bitbucket\`.
409
+ 2. **Always check for an existing verdict comment** before posting (idempotency).
410
+ 3. **Never post on a PR authored by the reviewer account** — filter \`pullrequest.author\` against the \`BITBUCKET_EMAIL\` identity to avoid loops.
411
+ 4. **Honor the diff-size gate** — if \`fetch_pr_diffstat\` shows a PR over the configured threshold, post a summary comment explaining the skip and **do not** approve or request changes.
412
+ 5. **Do not include secret values in comment bodies or logs.**
413
+
414
+ ---
415
+
416
+ ## Quick Reference
417
+
418
+ | Action | Endpoint | Method |
419
+ |--------|----------|--------|
420
+ | fetch_pr_diff | \`/pullrequests/{id}/diff\` | GET |
421
+ | fetch_pr_diffstat | \`/pullrequests/{id}/diffstat\` | GET |
422
+ | post_inline_comment | \`/pullrequests/{id}/comments\` | POST (with \`inline\`) |
423
+ | post_summary_comment | \`/pullrequests/{id}/comments\` | POST (no \`inline\`) |
424
+ | approve | \`/pullrequests/{id}/approve\` | POST |
425
+ | unapprove | \`/pullrequests/{id}/approve\` | DELETE |
426
+ | request_changes | \`/pullrequests/{id}/request-changes\` | POST |
427
+ | unrequest_changes | \`/pullrequests/{id}/request-changes\` | DELETE |
428
+ | list_existing_comments | \`/pullrequests/{id}/comments?pagelen=100\` | GET |
429
+
430
+ Base URL: \`https://api.bitbucket.org/2.0/repositories/{workspace}/{repo_slug}\``,
431
+ };