|
25008
|
NULL
|
0
|
2026-05-12T10:33:00.300341+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778581980300_m2.jpg...
|
Firefox
|
JY-20773 fix user pilot tracking ofr automated rep JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app — Work...
|
True
|
github.com/jiminny/app/pull/12024
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (4)
Security and quality
(
4
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
JY-20773 fix user pilot tracking for automated report generated #12024 Edit title
JY-20773 fix user pilot tracking for automated report generated
#
12024
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
LakyLak
LakyLak
wants to merge 2 commits into
master
master
from
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Copy head branch name to clipboard
Lines changed: 3 additions & 0 deletions
Conversation (1)
Conversation
(
1
)
Commits (2)
Commits
(
2
)
Checks (3)
Checks
(
3
)
Files changed (1)
Files changed
(
1
)
Conversation
Conversation
@LakyLak
Show options
LakyLak commented 2 weeks ago
LakyLak
LakyLak
commented
2 weeks ago
2 weeks ago
Write
Write
Preview
Preview
### JIRA: JY-20773
#### Changes:
- Add SerializesModels trait as it seems the UP needs that for successful
Cancel
Cancel
Update comment
Update comment
LakyLak
LakyLak
and others
added
2
commits
2 weeks ago
2 weeks ago
@LakyLak
JY-20773
JY-20773
fix user pilot tracking ofr automated report generated
fix user pilot tracking ofr automated report generated
8 / 10 checks OK
6bd41c8
6bd41c8
@LakyLak
Merge branch 'master' into
Merge branch 'master' into
JY-20773
JY-20773
-fix-automated-reports-user-pilot-…
-fix-automated-reports-user-pilot-…
…
Verified
Verified
12 / 12 checks OK
5b2c922
5b2c922
@sonarqubecloud
Show options
sonarqubecloud Bot commented 7 minutes ago
sonarqubecloud
sonarqubecloud
Bot
commented
7 minutes ago
7 minutes ago
Quality Gate Passed Quality Gate passed
Quality Gate Passed
Quality Gate passed
Issues
0 New issues
0 New issues
0 Accepted issues
0 Accepted issues
Measures
0 Security Hotspots
0 Security Hotspots
0.0% Coverage on New Code
0.0% Coverage on New Code
0.0% Duplication on New Code
0.0% Duplication on New Code
See analysis details on SonarQube Cloud
See analysis details on SonarQube Cloud
Add or remove reactions
@LakyLak
LakyLak
LakyLak
changed the title
JY-20773 fix user pilot tracking ofr automated report generated
JY-20773 fix user pilot tracking for automated report generated
now
now
This branch has not been deployed
This branch has not been deployed
No deployments
Merge info
Merge info
Review required
Review required
At least 1 approving review is required by reviewers with write access.
All checks have passed
All checks have passed
12 successful checks
Expand checks
This branch is out-of-date with the base branch
This branch is out-of-date with the base branch
Merge the latest changes from master into this branch. This merge commit will be associated with LakyLak.
Update branch
Update branch
Update branch options
Merging is blocked
Merging is blocked
At least 1 approving review is required by reviewers with write access.
Enable auto-merge
Enable auto-merge
Select merge method
You can also merge this with the command line.
View command line instructions.
Still in progress?
Convert to draft
Convert to draft
@LakyLak
Add a comment
Add a comment
Comment
Write
Write
Preview
Preview
Comment
Markdown is supported
Markdown
is supported
Paste, drop, or click to add files
Paste, drop, or click to add files
Close pull request
Close pull request
Comment
Remember, contributions to this repository should follow our
GitHub Community Guidelines
GitHub Community Guidelines
.
ProTip!
Add
.patch
.patch
or
.diff
.diff
to the end of URLs for Git’s plaintext views.
Reviewers
No reviews—at least 1 approving review is required.
Still in progress?
Convert to draft
Assignees
No one—
assign yourself
Labels
None yet
Projects
None yet
Milestone
No milestone
Development
Development
Successfully merging this pull request may close these issues.
None yet
Notifications Customize
Notifications
Customize
Unsubscribe
Unsubscribe
You’re receiving notifications because you’re watching this repository.
1 participant
1 participant
@LakyLak
Lock conversation
Lock conversation
Footer
Footer
GitHub Homepage
© 2026 GitHub, Inc.
Footer navigation
Footer navigation
Terms
Terms
Privacy
Privacy
Security
Security
Status
Status
Community
Community
Docs
Docs
Contact
Contact
Manage cookies
Do not share my personal information...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.057063047,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.0028257978,"top":0.08060654,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.015957447,"top":0.09217877,"width":0.40492022,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"CloudWatch | us-east-2","depth":4,"bounds":{"left":0.0028257978,"top":0.11332801,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CloudWatch | us-east-2","depth":5,"bounds":{"left":0.015957447,"top":0.12490024,"width":0.04138963,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.15123703,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"bounds":{"left":0.0028257978,"top":0.17478053,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"bounds":{"left":0.015957447,"top":0.18635276,"width":0.16888298,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"bounds":{"left":0.0,"top":0.207502,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.21907422,"width":0.16140293,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.24022347,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.25179568,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.27294493,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.28451717,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.3056664,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.31723863,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.33838788,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.3499601,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.37110934,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.38268158,"width":0.1931516,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.4038308,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.41540304,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0,"top":0.4365523,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.4481245,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.46927375,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.48084596,"width":0.15159574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Data Explorer","depth":4,"bounds":{"left":0.0,"top":0.5019952,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Data Explorer","depth":5,"bounds":{"left":0.013297873,"top":0.51356745,"width":0.0234375,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.53471667,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.5462889,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.5674381,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.57901037,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.60015965,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.6117318,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.6328811,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.6444533,"width":0.09524601,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Team - Backlog - Jira","depth":4,"bounds":{"left":0.0,"top":0.66560256,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team - Backlog - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.6771748,"width":0.053025264,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.698324,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.70989627,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.7310455,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.7426177,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.73822826,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.76376694,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.7753392,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.7980846,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"bounds":{"left":0.07962101,"top":0.0518755,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"bounds":{"left":0.07962101,"top":0.05347167,"width":0.0029920214,"height":0.21468475},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"bounds":{"left":0.08494016,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"bounds":{"left":0.099567816,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"bounds":{"left":0.112865694,"top":0.06464485,"width":0.018949468,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"bounds":{"left":0.11486037,"top":0.07063048,"width":0.014960106,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"bounds":{"left":0.13680187,"top":0.06464485,"width":0.017785905,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"bounds":{"left":0.13879654,"top":0.07063048,"width":0.008477394,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"bounds":{"left":0.81698805,"top":0.06464485,"width":0.06565824,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.82928854,"top":0.07063048,"width":0.011801862,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"bounds":{"left":0.8424202,"top":0.07222666,"width":0.002493351,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"bounds":{"left":0.84640956,"top":0.07063048,"width":0.021276595,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"bounds":{"left":0.88464093,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"bounds":{"left":0.8949468,"top":0.06464485,"width":0.008643617,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"bounds":{"left":0.9115692,"top":0.06464485,"width":0.01662234,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"All issues(g then i)","depth":9,"bounds":{"left":0.93085104,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All pull requests","depth":9,"bounds":{"left":0.94414896,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All repositories","depth":9,"bounds":{"left":0.9574468,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"bounds":{"left":0.97074467,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"bounds":{"left":0.9840425,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"bounds":{"left":0.079288565,"top":0.051077414,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"bounds":{"left":0.079288565,"top":0.05387071,"width":0.0787899,"height":0.023144454},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"bounds":{"left":0.08494016,"top":0.09936153,"width":0.025099734,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"bounds":{"left":0.095744684,"top":0.10574621,"width":0.011469414,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"bounds":{"left":0.11269947,"top":0.09936153,"width":0.05501995,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"bounds":{"left":0.12333777,"top":0.10574621,"width":0.02925532,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.15525267,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"bounds":{"left":0.15824468,"top":0.113727055,"width":0.0056515955,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.16389628,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"bounds":{"left":0.17037898,"top":0.09936153,"width":0.029089095,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"bounds":{"left":0.18134974,"top":0.10574621,"width":0.01512633,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"bounds":{"left":0.20212767,"top":0.09936153,"width":0.03025266,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"bounds":{"left":0.21326463,"top":0.10574621,"width":0.015957447,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"bounds":{"left":0.23503989,"top":0.09936153,"width":0.022938829,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"bounds":{"left":0.24601063,"top":0.10574621,"width":0.009142287,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (4)","depth":12,"bounds":{"left":0.2606383,"top":0.09936153,"width":0.06815159,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"bounds":{"left":0.27244017,"top":0.10574621,"width":0.04255319,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.31881648,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":14,"bounds":{"left":0.32180852,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.32480052,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"bounds":{"left":0.33144948,"top":0.09936153,"width":0.03125,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"bounds":{"left":0.34258643,"top":0.10574621,"width":0.016954787,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"bounds":{"left":0.36535904,"top":0.09936153,"width":0.032081116,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.37649602,"top":0.10574621,"width":0.017785905,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"bounds":{"left":0.09325133,"top":0.14365523,"width":0.0003324468,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.039228722,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.2159242,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"bounds":{"left":0.34973404,"top":0.1452514,"width":0.08261303,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.48454124,"top":0.1452514,"width":0.0013297872,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"bounds":{"left":0.98636967,"top":0.13886672,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"JY-20773 fix user pilot tracking for automated report generated #12024 Edit title","depth":13,"bounds":{"left":0.33776596,"top":0.1915403,"width":0.3151596,"height":0.06384677},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated","depth":14,"bounds":{"left":0.33776596,"top":0.19233839,"width":0.28656915,"height":0.030327214},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"bounds":{"left":0.34042552,"top":0.22426178,"width":0.006482713,"height":0.030327214},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12024","depth":15,"bounds":{"left":0.34690824,"top":0.22426178,"width":0.029421542,"height":0.030327214},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"bounds":{"left":0.3776596,"top":0.22665602,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"bounds":{"left":0.6555851,"top":0.19832402,"width":0.055518616,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"bounds":{"left":0.66788566,"top":0.20430966,"width":0.038896278,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"bounds":{"left":0.7137633,"top":0.19832402,"width":0.02825798,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"bounds":{"left":0.7180851,"top":0.20430966,"width":0.011635638,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"bounds":{"left":0.34840426,"top":0.2677574,"width":0.011968086,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"LakyLak","depth":15,"bounds":{"left":0.36702126,"top":0.26456505,"width":0.018450798,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":16,"bounds":{"left":0.36702126,"top":0.2661612,"width":0.018450798,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"bounds":{"left":0.38680187,"top":0.2661612,"width":0.06648936,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"bounds":{"left":0.45462102,"top":0.264166,"width":0.018284574,"height":0.017557861},"on_screen":true,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"bounds":{"left":0.4566157,"top":0.26735833,"width":0.014295213,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"bounds":{"left":0.4742354,"top":0.2661612,"width":0.009973404,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":16,"bounds":{"left":0.48553857,"top":0.264166,"width":0.12400266,"height":0.017557861},"on_screen":true,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":17,"bounds":{"left":0.48753324,"top":0.26735833,"width":0.1200133,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"bounds":{"left":0.610871,"top":0.26177174,"width":0.00930851,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 3 additions & 0 deletions","depth":14,"bounds":{"left":0.72041225,"top":0.3180367,"width":0.019946808,"height":0.11412609},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (1)","depth":16,"bounds":{"left":0.33776596,"top":0.30007982,"width":0.054022606,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"bounds":{"left":0.35006648,"top":0.30965683,"width":0.028091755,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.38746676,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"bounds":{"left":0.39045876,"top":0.30965683,"width":0.0021609042,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.39261967,"top":0.30965683,"width":0.0016622341,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"bounds":{"left":0.39178857,"top":0.30007982,"width":0.045545213,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"bounds":{"left":0.4040891,"top":0.30965683,"width":0.019115692,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.43301198,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"bounds":{"left":0.43600398,"top":0.30965683,"width":0.0028257978,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.43882978,"top":0.30965683,"width":0.0016622341,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"bounds":{"left":0.43733376,"top":0.30007982,"width":0.04255319,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"bounds":{"left":0.4496343,"top":0.30965683,"width":0.015957447,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.47556517,"top":0.30965683,"width":0.0028257978,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"bounds":{"left":0.47839096,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.48138297,"top":0.30965683,"width":0.0016622341,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"bounds":{"left":0.47988698,"top":0.30007982,"width":0.055684842,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"bounds":{"left":0.4921875,"top":0.30965683,"width":0.029753989,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.53125,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"bounds":{"left":0.53424203,"top":0.30965683,"width":0.0019946808,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.5362367,"top":0.30965683,"width":0.0018284575,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"bounds":{"left":0.33776596,"top":0.34557062,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"bounds":{"left":0.33776596,"top":0.34836394,"width":0.048204787,"height":0.023144454},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":12,"bounds":{"left":0.33776596,"top":0.34557062,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"bounds":{"left":0.61136967,"top":0.3463687,"width":0.007978723,"height":0.02952913},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"LakyLak commented 2 weeks ago","depth":14,"bounds":{"left":0.3620346,"top":0.3463687,"width":0.24135639,"height":0.02952913},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"LakyLak","depth":16,"bounds":{"left":0.3620346,"top":0.35434955,"width":0.018450798,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":17,"bounds":{"left":0.3620346,"top":0.35434955,"width":0.018450798,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"bounds":{"left":0.38181517,"top":0.35434955,"width":0.025598405,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 weeks ago","depth":15,"bounds":{"left":0.40874335,"top":0.3527534,"width":0.026928192,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 weeks ago","depth":17,"bounds":{"left":0.40874335,"top":0.35434955,"width":0.026928192,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Write","depth":17,"bounds":{"left":0.359375,"top":0.3830806,"width":0.022606382,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Write","depth":18,"bounds":{"left":0.3650266,"top":0.3926576,"width":0.011303191,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Preview","depth":17,"bounds":{"left":0.38198137,"top":0.3830806,"width":0.028091755,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Preview","depth":18,"bounds":{"left":0.38763297,"top":0.3926576,"width":0.016788565,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextArea","text":"### JIRA: JY-20773\n\n#### Changes:\n- Add SerializesModels trait as it seems the UP needs that for successful","depth":19,"bounds":{"left":0.36269948,"top":0.42218676,"width":0.25598404,"height":0.096568234},"on_screen":true,"value":"### JIRA: JY-20773\n\n#### Changes:\n- Add SerializesModels trait as it seems the UP needs that for successful","help_text":"","placeholder":"Leave a comment","role_description":"text entry area","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Cancel","depth":16,"bounds":{"left":0.55019945,"top":0.5331205,"width":0.023769947,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Cancel","depth":18,"bounds":{"left":0.55452126,"top":0.53910613,"width":0.01512633,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Update comment","depth":16,"bounds":{"left":0.5752992,"top":0.5331205,"width":0.046708778,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Update comment","depth":18,"bounds":{"left":0.579621,"top":0.53910613,"width":0.038065158,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"LakyLak","depth":14,"bounds":{"left":0.3700133,"top":0.5969673,"width":0.018450798,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":15,"bounds":{"left":0.3700133,"top":0.5969673,"width":0.018450798,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and others","depth":14,"bounds":{"left":0.38979387,"top":0.5969673,"width":0.023105053,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"added","depth":14,"bounds":{"left":0.41289893,"top":0.5969673,"width":0.016123671,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":14,"bounds":{"left":0.4290226,"top":0.5969673,"width":0.0026595744,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commits","depth":14,"bounds":{"left":0.43168217,"top":0.5969673,"width":0.021110373,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 weeks ago","depth":14,"bounds":{"left":0.45279256,"top":0.5969673,"width":0.026761968,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 weeks ago","depth":16,"bounds":{"left":0.45279256,"top":0.5969673,"width":0.026761968,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":12,"bounds":{"left":0.3700133,"top":0.6368715,"width":0.0066489363,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"JY-20773","depth":14,"bounds":{"left":0.37865692,"top":0.6404629,"width":0.019115692,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773","depth":15,"bounds":{"left":0.37865692,"top":0.6404629,"width":0.019115692,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"fix user pilot tracking ofr automated report generated","depth":14,"bounds":{"left":0.40009972,"top":0.6404629,"width":0.12965426,"height":0.011572227},"on_screen":true,"help_text":"JY-20773 fix user pilot tracking ofr automated report generated","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix user pilot tracking ofr automated report generated","depth":15,"bounds":{"left":0.40009972,"top":0.6404629,"width":0.12965426,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"8 / 10 checks OK","depth":14,"bounds":{"left":0.60139626,"top":0.6368715,"width":0.005319149,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"6bd41c8","depth":14,"bounds":{"left":0.6080452,"top":0.6404629,"width":0.016954787,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6bd41c8","depth":15,"bounds":{"left":0.6080452,"top":0.6404629,"width":0.016954787,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":12,"bounds":{"left":0.3700133,"top":0.6783719,"width":0.0066489363,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Merge branch 'master' into","depth":14,"bounds":{"left":0.37865692,"top":0.68196326,"width":0.062333778,"height":0.011572227},"on_screen":true,"help_text":"Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Merge branch 'master' into","depth":15,"bounds":{"left":0.37865692,"top":0.68196326,"width":0.062333778,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773","depth":14,"bounds":{"left":0.44331783,"top":0.68196326,"width":0.019281914,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773","depth":15,"bounds":{"left":0.44331783,"top":0.68196326,"width":0.019281914,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"-fix-automated-reports-user-pilot-…","depth":14,"bounds":{"left":0.46259972,"top":0.68196326,"width":0.083942816,"height":0.011572227},"on_screen":true,"help_text":"Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"-fix-automated-reports-user-pilot-…","depth":15,"bounds":{"left":0.46259972,"top":0.68196326,"width":0.083942816,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"…","depth":14,"bounds":{"left":0.5505319,"top":0.6827614,"width":0.006981383,"height":0.009577015},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Verified","depth":13,"bounds":{"left":0.57862365,"top":0.6803671,"width":0.020113032,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Verified","depth":15,"bounds":{"left":0.5812833,"top":0.6823623,"width":0.014793883,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"12 / 12 checks OK","depth":14,"bounds":{"left":0.60139626,"top":0.6783719,"width":0.005319149,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"5b2c922","depth":14,"bounds":{"left":0.6080452,"top":0.68196326,"width":0.016954787,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5b2c922","depth":15,"bounds":{"left":0.6080452,"top":0.68196326,"width":0.016954787,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@sonarqubecloud","depth":13,"bounds":{"left":0.33776596,"top":0.7254589,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.72625697,"width":0.007978723,"height":0.02952913},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"sonarqubecloud Bot commented 7 minutes ago","depth":13,"bounds":{"left":0.3620346,"top":0.72625697,"width":0.24135639,"height":0.029928172},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"sonarqubecloud","depth":15,"bounds":{"left":0.3620346,"top":0.73423785,"width":0.036236703,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sonarqubecloud","depth":16,"bounds":{"left":0.3620346,"top":0.73423785,"width":0.036236703,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bot","depth":16,"bounds":{"left":0.40176198,"top":0.735834,"width":0.0066489363,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.41206783,"top":0.73423785,"width":0.025598405,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"7 minutes ago","depth":14,"bounds":{"left":0.43899602,"top":0.73264164,"width":0.03025266,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 minutes ago","depth":16,"bounds":{"left":0.43899602,"top":0.73423785,"width":0.03025266,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Quality Gate Passed Quality Gate passed","depth":16,"bounds":{"left":0.3620346,"top":0.7697526,"width":0.25731382,"height":0.026735835},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"Quality Gate Passed","depth":17,"bounds":{"left":0.3620346,"top":0.7701516,"width":0.0066489363,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Quality Gate passed","depth":18,"bounds":{"left":0.37034574,"top":0.7701516,"width":0.06416223,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Issues","depth":17,"bounds":{"left":0.3620346,"top":0.81085396,"width":0.013464096,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 New issues","depth":17,"bounds":{"left":0.36851728,"top":0.8276137,"width":0.028590426,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0 New issues","depth":18,"bounds":{"left":0.36851728,"top":0.8276137,"width":0.028590426,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Accepted issues","depth":17,"bounds":{"left":0.36851728,"top":0.8443735,"width":0.03956117,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0 Accepted issues","depth":18,"bounds":{"left":0.36851728,"top":0.8443735,"width":0.03956117,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Measures","depth":17,"bounds":{"left":0.3620346,"top":0.8739026,"width":0.020777926,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Security Hotspots","depth":17,"bounds":{"left":0.36851728,"top":0.89066243,"width":0.04288564,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0 Security Hotspots","depth":18,"bounds":{"left":0.36851728,"top":0.89066243,"width":0.04288564,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0.0% Coverage on New Code","depth":17,"bounds":{"left":0.36851728,"top":0.9074222,"width":0.0631649,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0.0% Coverage on New Code","depth":18,"bounds":{"left":0.36851728,"top":0.9074222,"width":0.0631649,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0.0% Duplication on New Code","depth":17,"bounds":{"left":0.36851728,"top":0.92418194,"width":0.066821806,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0.0% Duplication on New Code","depth":18,"bounds":{"left":0.36851728,"top":0.92418194,"width":0.066821806,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"See analysis details on SonarQube Cloud","depth":17,"bounds":{"left":0.3620346,"top":0.9537111,"width":0.087932184,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"See analysis details on SonarQube Cloud","depth":18,"bounds":{"left":0.3620346,"top":0.9537111,"width":0.087932184,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"bounds":{"left":0.3620346,"top":0.98164403,"width":0.008643617,"height":0.018355966},"on_screen":true,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@LakyLak","depth":14,"bounds":{"left":0.3700133,"top":1.0,"width":0.0066489363,"height":-0.04549086},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"LakyLak","depth":14,"bounds":{"left":0.3778258,"top":1.0,"width":0.01861702,"height":-0.047086954},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":15,"bounds":{"left":0.3778258,"top":1.0,"width":0.01861702,"height":-0.047086954},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"changed the title","depth":14,"bounds":{"left":0.39644283,"top":1.0,"width":0.038896278,"height":-0.047086954},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated","depth":15,"bounds":{"left":0.4353391,"top":1.0,"width":0.1434508,"height":-0.047086954},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated","depth":15,"bounds":{"left":0.3700133,"top":1.0,"width":0.25,"height":-0.047086954},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"now","depth":14,"bounds":{"left":0.47340426,"top":1.0,"width":0.00880984,"height":-0.065043926},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"now","depth":16,"bounds":{"left":0.47340426,"top":1.0,"width":0.00880984,"height":-0.065043926},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"This branch has not been deployed","depth":14,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This branch has not been deployed","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No deployments","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Merge info","depth":12,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Merge info","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Review required","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review required","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"At least 1 approving review is required by reviewers with write access.","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"All checks have passed","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"All checks have passed","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12 successful checks","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Expand checks","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"This branch is out-of-date with the base branch","depth":14,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This branch is out-of-date with the base branch","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Merge the latest changes from master into this branch. This merge commit will be associated with LakyLak.","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Update branch","depth":14,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Update branch","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Update branch options","depth":15,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Merging is blocked","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Merging is blocked","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"At least 1 approving review is required by reviewers with write access.","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Enable auto-merge","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enable auto-merge","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Select merge method","depth":13,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"You can also merge this with the command line.","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"View command line instructions.","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Still in progress?","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Convert to draft","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Convert to draft","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Add a comment","depth":17,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Add a comment","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Write","depth":18,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Write","depth":19,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Preview","depth":18,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Preview","depth":19,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextArea","text":"Comment","depth":20,"on_screen":false,"placeholder":" ","role_description":"text entry area","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Markdown is supported","depth":19,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Markdown","depth":21,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is supported","depth":21,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Paste, drop, or click to add files","depth":18,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Paste, drop, or click to add files","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close pull request","depth":17,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Close pull request","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Comment","depth":17,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Remember, contributions to this repository should follow our","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub Community Guidelines","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub Community Guidelines","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ProTip!","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Add","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".patch","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".patch","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"or","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".diff","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".diff","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to the end of URLs for Git’s plaintext views.","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Reviewers","depth":16,"bounds":{"left":0.6356383,"top":0.34557062,"width":0.10638298,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"No reviews—at least 1 approving review is required.","depth":15,"bounds":{"left":0.6356383,"top":0.37071028,"width":0.09624335,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Still in progress?","depth":16,"bounds":{"left":0.6356383,"top":0.3914605,"width":0.031083776,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Convert to draft","depth":16,"bounds":{"left":0.66805184,"top":0.39026338,"width":0.029920213,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Assignees","depth":16,"bounds":{"left":0.6356383,"top":0.43415803,"width":0.10638298,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"No one—","depth":15,"bounds":{"left":0.6356383,"top":0.4592977,"width":0.016788565,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"assign yourself","depth":15,"bounds":{"left":0.65242684,"top":0.45810056,"width":0.02825798,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Labels","depth":15,"bounds":{"left":0.6356383,"top":0.49561054,"width":0.10638298,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"None yet","depth":14,"bounds":{"left":0.6356383,"top":0.5207502,"width":0.016788565,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Projects","depth":16,"bounds":{"left":0.6356383,"top":0.55706304,"width":0.10638298,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"None yet","depth":15,"bounds":{"left":0.6356383,"top":0.58220273,"width":0.016788565,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Milestone","depth":16,"bounds":{"left":0.6356383,"top":0.61851555,"width":0.10638298,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"No milestone","depth":15,"bounds":{"left":0.6356383,"top":0.64365524,"width":0.024601065,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Development","depth":17,"bounds":{"left":0.6356383,"top":0.6831604,"width":0.10638298,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Development","depth":18,"bounds":{"left":0.6356383,"top":0.6843575,"width":0.025764627,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Successfully merging this pull request may close these issues.","depth":18,"bounds":{"left":0.6356383,"top":0.70510775,"width":0.10305851,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"None yet","depth":18,"bounds":{"left":0.6356383,"top":0.7418196,"width":0.016788565,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications Customize","depth":15,"bounds":{"left":0.6356383,"top":0.7781325,"width":0.10638298,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":17,"bounds":{"left":0.6356383,"top":0.78252196,"width":0.025099734,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Customize","depth":17,"bounds":{"left":0.72207445,"top":0.78252196,"width":0.019946808,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unsubscribe","depth":16,"bounds":{"left":0.6356383,"top":0.802075,"width":0.10638298,"height":0.022346368},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Unsubscribe","depth":18,"bounds":{"left":0.68002,"top":0.8072626,"width":0.024102394,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"You’re receiving notifications because you’re watching this repository.","depth":16,"bounds":{"left":0.6356383,"top":0.8288109,"width":0.1022274,"height":0.026336791},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"1 participant","depth":15,"bounds":{"left":0.6356383,"top":0.88268155,"width":0.10638298,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1 participant","depth":16,"bounds":{"left":0.6356383,"top":0.8838787,"width":0.02443484,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":15,"bounds":{"left":0.6356383,"top":0.9066241,"width":0.008643617,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Lock conversation","depth":15,"bounds":{"left":0.6356383,"top":0.9537111,"width":0.04338431,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lock conversation","depth":17,"bounds":{"left":0.64328456,"top":0.9549082,"width":0.035738032,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Footer","depth":7,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Footer","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub Homepage","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"© 2026 GitHub, Inc.","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Footer navigation","depth":8,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Footer navigation","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Terms","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Terms","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Privacy","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Privacy","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Status","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Status","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Community","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Community","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Docs","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Docs","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Contact","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Contact","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Manage cookies","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Do not share my personal information","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7249413248777433562
|
-2443966840503109496
|
idle
|
accessibility
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (4)
Security and quality
(
4
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
JY-20773 fix user pilot tracking for automated report generated #12024 Edit title
JY-20773 fix user pilot tracking for automated report generated
#
12024
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
LakyLak
LakyLak
wants to merge 2 commits into
master
master
from
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Copy head branch name to clipboard
Lines changed: 3 additions & 0 deletions
Conversation (1)
Conversation
(
1
)
Commits (2)
Commits
(
2
)
Checks (3)
Checks
(
3
)
Files changed (1)
Files changed
(
1
)
Conversation
Conversation
@LakyLak
Show options
LakyLak commented 2 weeks ago
LakyLak
LakyLak
commented
2 weeks ago
2 weeks ago
Write
Write
Preview
Preview
### JIRA: JY-20773
#### Changes:
- Add SerializesModels trait as it seems the UP needs that for successful
Cancel
Cancel
Update comment
Update comment
LakyLak
LakyLak
and others
added
2
commits
2 weeks ago
2 weeks ago
@LakyLak
JY-20773
JY-20773
fix user pilot tracking ofr automated report generated
fix user pilot tracking ofr automated report generated
8 / 10 checks OK
6bd41c8
6bd41c8
@LakyLak
Merge branch 'master' into
Merge branch 'master' into
JY-20773
JY-20773
-fix-automated-reports-user-pilot-…
-fix-automated-reports-user-pilot-…
…
Verified
Verified
12 / 12 checks OK
5b2c922
5b2c922
@sonarqubecloud
Show options
sonarqubecloud Bot commented 7 minutes ago
sonarqubecloud
sonarqubecloud
Bot
commented
7 minutes ago
7 minutes ago
Quality Gate Passed Quality Gate passed
Quality Gate Passed
Quality Gate passed
Issues
0 New issues
0 New issues
0 Accepted issues
0 Accepted issues
Measures
0 Security Hotspots
0 Security Hotspots
0.0% Coverage on New Code
0.0% Coverage on New Code
0.0% Duplication on New Code
0.0% Duplication on New Code
See analysis details on SonarQube Cloud
See analysis details on SonarQube Cloud
Add or remove reactions
@LakyLak
LakyLak
LakyLak
changed the title
JY-20773 fix user pilot tracking ofr automated report generated
JY-20773 fix user pilot tracking for automated report generated
now
now
This branch has not been deployed
This branch has not been deployed
No deployments
Merge info
Merge info
Review required
Review required
At least 1 approving review is required by reviewers with write access.
All checks have passed
All checks have passed
12 successful checks
Expand checks
This branch is out-of-date with the base branch
This branch is out-of-date with the base branch
Merge the latest changes from master into this branch. This merge commit will be associated with LakyLak.
Update branch
Update branch
Update branch options
Merging is blocked
Merging is blocked
At least 1 approving review is required by reviewers with write access.
Enable auto-merge
Enable auto-merge
Select merge method
You can also merge this with the command line.
View command line instructions.
Still in progress?
Convert to draft
Convert to draft
@LakyLak
Add a comment
Add a comment
Comment
Write
Write
Preview
Preview
Comment
Markdown is supported
Markdown
is supported
Paste, drop, or click to add files
Paste, drop, or click to add files
Close pull request
Close pull request
Comment
Remember, contributions to this repository should follow our
GitHub Community Guidelines
GitHub Community Guidelines
.
ProTip!
Add
.patch
.patch
or
.diff
.diff
to the end of URLs for Git’s plaintext views.
Reviewers
No reviews—at least 1 approving review is required.
Still in progress?
Convert to draft
Assignees
No one—
assign yourself
Labels
None yet
Projects
None yet
Milestone
No milestone
Development
Development
Successfully merging this pull request may close these issues.
None yet
Notifications Customize
Notifications
Customize
Unsubscribe
Unsubscribe
You’re receiving notifications because you’re watching this repository.
1 participant
1 participant
@LakyLak
Lock conversation
Lock conversation
Footer
Footer
GitHub Homepage
© 2026 GitHub, Inc.
Footer navigation
Footer navigation
Terms
Terms
Privacy
Privacy
Security
Security
Status
Status
Community
Community
Docs
Docs
Contact
Contact
Manage cookies
Do not share my personal information...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
21890
|
958
|
7
|
2026-05-12T06:33:33.687323+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778567613687_m1.jpg...
|
Firefox
|
JY-20725 add HS rate limit handling on activities JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app — Work...
|
True
|
github.com/jiminny/app/pull/12066
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.067708336,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.090625,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.11388889,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.13715278,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.16041666,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"All issues(g then i)","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7247362109286038679
|
-2723249286657361266
|
click
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)...
|
21888
|
NULL
|
NULL
|
NULL
|
|
21366
|
935
|
28
|
2026-05-11T17:46:22.697739+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778521582697_m2.jpg...
|
Firefox
|
SQLite Web: db.sqlite — Personal
|
True
|
sqlite.screenpipe.lakylak.xyz/frames/
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Screenpipe — Archive
Screenpipe — New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
All docs · AFFiNE
All docs · AFFiNE
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
sqlite-web 0.7.2
sqlite-web 0.7.2
db.sqlite
db.sqlite
frames
Query
Query
table name...
_sqlx_migrations
_sqlx_migrations
audio_chunks
audio_chunks
audio_tags
audio_tags
audio_transcriptions
audio_transcriptions
audio_transcriptions_fts (v)
audio_transcriptions_fts
(v)
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
elements
elements
elements_fts (v)
elements_fts
(v)
elements_fts_config
elements_fts_config
elements_fts_data
elements_fts_data
elements_fts_idx
elements_fts_idx
frames
frames
frames_fts (v)
frames_fts
(v)
frames_fts_config
frames_fts_config
frames_fts_data
frames_fts_data
frames_fts_idx
frames_fts_idx
meetings
meetings
memories
memories
memories_fts (v)
memories_fts
(v)
memories_fts_config
memories_fts_config
memories_fts_data
memories_fts_data
memories_fts_idx
memories_fts_idx
ocr_text
ocr_text
pipe_executions
pipe_executions
pipe_scheduler_state
pipe_scheduler_state
secrets
secrets
speaker_embeddings
speaker_embeddings
speakers
speakers
sqlite_sequence
sqlite_sequence
sqlite_stat1
sqlite_stat1
sqlite_stat4
sqlite_stat4
tags
tags
ui_events
ui_events
ui_events_fts (v)
ui_events_fts
(v)
ui_events_fts_config
ui_events_fts_config
ui_events_fts_data
ui_events_fts_data
ui_events_fts_idx
ui_events_fts_idx
video_chunks
video_chunks
vision_tags
vision_tags
Toggle helper tables
Toggle helper tables
Structure
Structure
Content
Content
Query
Query
Export
Export
SQL
SQL
CREATE
TABLE
"frames"
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT
,
video_chunk_id
INTEGER
DEFAULT
NULL
,
offset_index
INTEGER
NOT
NULL
DEFAULT
0
,
timestamp
TIMESTAMP
NOT
NULL
,
name
TEXT
,
app_name
TEXT
DEFAULT
NULL
,
window_name
TEXT
DEFAULT
NULL
,
focused
BOOLEAN
DEFAULT
NULL
,
browser_url
TEXT
DEFAULT
NULL
,
device_name
TEXT
NOT
NULL
DEFAULT
''
,
sync_id
TEXT
,
machine_id
TEXT
,
synced_at
DATETIME
,
-- New event-driven capture columns
snapshot_path
TEXT
DEFAULT
NULL
,
accessibility_text
TEXT
DEFAULT
NULL
,
accessibility_tree_json
TEXT
DEFAULT
NULL
,
content_hash
INTEGER
DEFAULT
NULL
,
simhash
INTEGER
DEFAULT
NULL
,
capture_trigger
TEXT
DEFAULT
NULL
,
text_source
TEXT
DEFAULT
NULL
,
cloud_blob_id
TEXT
DEFAULT
NULL
,
full_text
TEXT
DEFAULT
NULL
,
elements_ref_frame_id
INTEGER
DEFAULT
NULL
,
document_path
TEXT
DEFAULT
NULL
,
accessibility_redacted_at
INTEGER
,
image_redacted_at
INTEGER
,
FOREIGN
KEY
(
video_chunk_id
)
REFERENCES
video_chunks
(
id
)
)
Columns
Columns
Column
Data type
Allow null
Primary key
Actions
id
INTEGER
✓
✓
read-only
video_chunk_id
INTEGER
✓
read-only
offset_index
INTEGER
read-only
timestamp
TIMESTAMP
read-only
name
TEXT
✓
read-only
app_name
TEXT
✓
read-only
window_name
TEXT
✓
read-only
focused
BOOLEAN
✓
read-only
browser_url
TEXT
✓
read-only
device_name
TEXT...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.5,"top":0.0518755,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.51329786,"top":0.06304868,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.5,"top":0.08459697,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.51329786,"top":0.09577015,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.5,"top":0.11731844,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.51329786,"top":0.12849163,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.5,"top":0.15003991,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.51329786,"top":0.16121309,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.5,"top":0.18276137,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.51329786,"top":0.19393456,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.5,"top":0.21548285,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.51329786,"top":0.22665602,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.5,"top":0.2482043,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.51329786,"top":0.25937748,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.5,"top":0.28092578,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.51329786,"top":0.29209897,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.55651593,"top":0.28810853,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.5028258,"top":0.31524342,"width":0.06333112,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.5028258,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.51379657,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.5249335,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.53607047,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.5472075,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"sqlite-web 0.7.2","depth":7,"bounds":{"left":0.57413566,"top":0.058260176,"width":0.043218084,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite-web 0.7.2","depth":8,"bounds":{"left":0.57413566,"top":0.06464485,"width":0.043218084,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"db.sqlite","depth":10,"bounds":{"left":0.62267286,"top":0.05865922,"width":0.023936171,"height":0.029928172},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"db.sqlite","depth":11,"bounds":{"left":0.6253325,"top":0.066640064,"width":0.01861702,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"frames","depth":10,"bounds":{"left":0.64660907,"top":0.066640064,"width":0.01512633,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Query","depth":8,"bounds":{"left":0.97639626,"top":0.061851557,"width":0.018284574,"height":0.023543496},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Query","depth":9,"bounds":{"left":0.9793883,"top":0.06743815,"width":0.012300532,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextField","text":"table name...","depth":7,"bounds":{"left":0.5738032,"top":0.111332804,"width":0.061835106,"height":0.023942538},"on_screen":true,"help_text":"","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"_sqlx_migrations","depth":9,"bounds":{"left":0.5738032,"top":0.13527533,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_sqlx_migrations","depth":10,"bounds":{"left":0.57712764,"top":0.13926576,"width":0.038896278,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_chunks","depth":9,"bounds":{"left":0.5738032,"top":0.15802075,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_chunks","depth":10,"bounds":{"left":0.57712764,"top":0.16201118,"width":0.031416222,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_tags","depth":9,"bounds":{"left":0.5738032,"top":0.18076617,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_tags","depth":10,"bounds":{"left":0.57712764,"top":0.18475658,"width":0.025099734,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions","depth":9,"bounds":{"left":0.5738032,"top":0.20351157,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions","depth":10,"bounds":{"left":0.57712764,"top":0.207502,"width":0.04654255,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.22625698,"width":0.06200133,"height":0.040702313},"on_screen":true,"help_text":"audio_transcriptions_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_fts","depth":10,"bounds":{"left":0.57712764,"top":0.23024741,"width":0.055352394,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.57712764,"top":0.24660814,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_...","depth":9,"bounds":{"left":0.5738032,"top":0.2669593,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"audio_transcriptions_fts_config","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_...","depth":10,"bounds":{"left":0.57712764,"top":0.27094972,"width":0.053523935,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_...","depth":9,"bounds":{"left":0.5738032,"top":0.2897047,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"audio_transcriptions_fts_data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_...","depth":10,"bounds":{"left":0.57712764,"top":0.29369512,"width":0.053523935,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_...","depth":9,"bounds":{"left":0.5738032,"top":0.3124501,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"audio_transcriptions_fts_idx","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_...","depth":10,"bounds":{"left":0.57712764,"top":0.31644055,"width":0.053523935,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements","depth":9,"bounds":{"left":0.5738032,"top":0.33519554,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements","depth":10,"bounds":{"left":0.57712764,"top":0.33918595,"width":0.020777926,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.35794094,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"elements_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts","depth":10,"bounds":{"left":0.57712764,"top":0.36193135,"width":0.030917553,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6080452,"top":0.3603352,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.38068634,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.38467678,"width":0.04637633,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.40343177,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts_data","depth":10,"bounds":{"left":0.57712764,"top":0.40742218,"width":0.042220745,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":0.42617717,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":0.4301676,"width":0.0390625,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames","depth":9,"bounds":{"left":0.5738032,"top":0.44892257,"width":0.06200133,"height":0.02434158},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames","depth":10,"bounds":{"left":0.5774601,"top":0.4537111,"width":0.015791224,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.47326416,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"frames_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts","depth":10,"bounds":{"left":0.57712764,"top":0.4772546,"width":0.025930852,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6030585,"top":0.47565842,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.4960096,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.5,"width":0.04138963,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.51875496,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts_data","depth":10,"bounds":{"left":0.57712764,"top":0.52274543,"width":0.03723404,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":0.5415004,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":0.5454908,"width":0.033909574,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"meetings","depth":9,"bounds":{"left":0.5738032,"top":0.5642458,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"meetings","depth":10,"bounds":{"left":0.57712764,"top":0.56823623,"width":0.020944148,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories","depth":9,"bounds":{"left":0.5738032,"top":0.58699125,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories","depth":10,"bounds":{"left":0.57712764,"top":0.59098166,"width":0.02244016,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.6097366,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"memories_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts","depth":10,"bounds":{"left":0.57712764,"top":0.61372703,"width":0.032579787,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6097075,"top":0.6121309,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.63248205,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.63647246,"width":0.048038565,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.6552275,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts_data","depth":10,"bounds":{"left":0.57712764,"top":0.6592179,"width":0.043882977,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":0.67797285,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":0.68196326,"width":0.04055851,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ocr_text","depth":9,"bounds":{"left":0.5738032,"top":0.7007183,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ocr_text","depth":10,"bounds":{"left":0.57712764,"top":0.7047087,"width":0.018783245,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"pipe_executions","depth":9,"bounds":{"left":0.5738032,"top":0.7234637,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"pipe_executions","depth":10,"bounds":{"left":0.57712764,"top":0.7274541,"width":0.036901597,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"pipe_scheduler_state","depth":9,"bounds":{"left":0.5738032,"top":0.7462091,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"pipe_scheduler_state","depth":10,"bounds":{"left":0.57712764,"top":0.7501995,"width":0.049035903,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secrets","depth":9,"bounds":{"left":0.5738032,"top":0.7689545,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secrets","depth":10,"bounds":{"left":0.57712764,"top":0.7729449,"width":0.016788565,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"speaker_embeddings","depth":9,"bounds":{"left":0.5738032,"top":0.79169995,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"speaker_embeddings","depth":10,"bounds":{"left":0.57712764,"top":0.79569036,"width":0.04886968,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"speakers","depth":9,"bounds":{"left":0.5738032,"top":0.8144453,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"speakers","depth":10,"bounds":{"left":0.57712764,"top":0.8184357,"width":0.020611702,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"sqlite_sequence","depth":9,"bounds":{"left":0.5738032,"top":0.83719075,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite_sequence","depth":10,"bounds":{"left":0.57712764,"top":0.84118116,"width":0.03723404,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"sqlite_stat1","depth":9,"bounds":{"left":0.5738032,"top":0.8599362,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite_stat1","depth":10,"bounds":{"left":0.57712764,"top":0.8639266,"width":0.025930852,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"sqlite_stat4","depth":9,"bounds":{"left":0.5738032,"top":0.88268155,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite_stat4","depth":10,"bounds":{"left":0.57712764,"top":0.88667196,"width":0.026595745,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"tags","depth":9,"bounds":{"left":0.5738032,"top":0.905427,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"tags","depth":10,"bounds":{"left":0.57712764,"top":0.9094174,"width":0.009973404,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events","depth":9,"bounds":{"left":0.5738032,"top":0.9281724,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events","depth":10,"bounds":{"left":0.57712764,"top":0.9321628,"width":0.021775266,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.9509178,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"ui_events_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts","depth":10,"bounds":{"left":0.57712764,"top":0.9549082,"width":0.031914894,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6090425,"top":0.95331204,"width":0.0048204786,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.9736632,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.9776536,"width":0.04737367,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.99640864,"width":0.06200133,"height":0.0035913587},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts_data","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.043218084,"height":-0.0003989935},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":1.0,"width":0.06200133,"height":-0.019154072},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.039893616,"height":-0.023144484},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"video_chunks","depth":9,"bounds":{"left":0.5738032,"top":1.0,"width":0.06200133,"height":-0.041899443},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"video_chunks","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.03125,"height":-0.045889854},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"vision_tags","depth":9,"bounds":{"left":0.5738032,"top":1.0,"width":0.06200133,"height":-0.06464481},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"vision_tags","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.025930852,"height":-0.068635225},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Toggle helper tables","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Toggle helper tables","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Structure","depth":9,"bounds":{"left":0.6456117,"top":0.111332804,"width":0.03274601,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Structure","depth":10,"bounds":{"left":0.6512633,"top":0.12011173,"width":0.02144282,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Content","depth":9,"bounds":{"left":0.6783577,"top":0.111332804,"width":0.029421542,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Content","depth":10,"bounds":{"left":0.6840093,"top":0.12011173,"width":0.018118352,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Query","depth":9,"bounds":{"left":0.7077792,"top":0.111332804,"width":0.025265958,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Query","depth":10,"bounds":{"left":0.7134308,"top":0.12011173,"width":0.013962766,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Export","depth":9,"bounds":{"left":0.7330452,"top":0.111332804,"width":0.026097074,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Export","depth":10,"bounds":{"left":0.7386968,"top":0.12011173,"width":0.014793883,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"SQL","depth":7,"bounds":{"left":0.6456117,"top":0.15961692,"width":0.3494016,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQL","depth":8,"bounds":{"left":0.6456117,"top":0.15961692,"width":0.016289894,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CREATE","depth":8,"bounds":{"left":0.6456117,"top":0.19273743,"width":0.015791224,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TABLE","depth":8,"bounds":{"left":0.6640625,"top":0.19273743,"width":0.013131649,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"frames\"","depth":8,"bounds":{"left":0.67985374,"top":0.19273743,"width":0.020944148,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":8,"bounds":{"left":0.70611703,"top":0.19273743,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":8,"bounds":{"left":0.6509308,"top":0.20830008,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.6587433,"top":0.20830008,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":8,"bounds":{"left":0.67985374,"top":0.20830008,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"KEY","depth":8,"bounds":{"left":0.70079786,"top":0.20830008,"width":0.007978723,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AUTOINCREMENT","depth":8,"bounds":{"left":0.71127,"top":0.20830008,"width":0.034242023,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.74551195,"top":0.20830008,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"video_chunk_id","depth":8,"bounds":{"left":0.6509308,"top":0.22386272,"width":0.03673537,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.6903258,"top":0.22386272,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.71127,"top":0.22386272,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.73238033,"top":0.22386272,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7428524,"top":0.22386272,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"offset_index","depth":8,"bounds":{"left":0.6509308,"top":0.23982441,"width":0.03158245,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.6850067,"top":0.23982441,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOT","depth":8,"bounds":{"left":0.70611703,"top":0.23982441,"width":0.0078125,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7165891,"top":0.23982441,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.7297208,"top":0.23982441,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"0","depth":8,"bounds":{"left":0.7506649,"top":0.23982441,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.75332445,"top":0.23982441,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":8,"bounds":{"left":0.6509308,"top":0.25538707,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":8,"bounds":{"left":0.6771942,"top":0.25538707,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOT","depth":8,"bounds":{"left":0.7034575,"top":0.25538707,"width":0.0078125,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.71392953,"top":0.25538707,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7244016,"top":0.25538707,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"name","depth":8,"bounds":{"left":0.6509308,"top":0.27134877,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6640625,"top":0.27134877,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.67453456,"top":0.27134877,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app_name","depth":8,"bounds":{"left":0.6509308,"top":0.2869114,"width":0.020944148,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.67453456,"top":0.2869114,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.68766624,"top":0.2869114,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7087766,"top":0.2869114,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.71924865,"top":0.2869114,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"window_name","depth":8,"bounds":{"left":0.6509308,"top":0.30247405,"width":0.028922873,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6825133,"top":0.30247405,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.695645,"top":0.30247405,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7165891,"top":0.30247405,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.72706115,"top":0.30247405,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"focused","depth":8,"bounds":{"left":0.6509308,"top":0.31843576,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BOOLEAN","depth":8,"bounds":{"left":0.671875,"top":0.31843576,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.69298536,"top":0.31843576,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.71392953,"top":0.31843576,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7244016,"top":0.31843576,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"browser_url","depth":8,"bounds":{"left":0.6509308,"top":0.3339984,"width":0.028922873,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6825133,"top":0.3339984,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.695645,"top":0.3339984,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7165891,"top":0.3339984,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.72706115,"top":0.3339984,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"device_name","depth":8,"bounds":{"left":0.6509308,"top":0.34956107,"width":0.028922873,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6825133,"top":0.34956107,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOT","depth":8,"bounds":{"left":0.695645,"top":0.34956107,"width":0.0078125,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.70611703,"top":0.34956107,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.71924865,"top":0.34956107,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":8,"bounds":{"left":0.74019283,"top":0.34956107,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.74551195,"top":0.34956107,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sync_id","depth":8,"bounds":{"left":0.6509308,"top":0.36552274,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.671875,"top":0.36552274,"width":0.010638298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.6825133,"top":0.36552274,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"machine_id","depth":8,"bounds":{"left":0.6509308,"top":0.3810854,"width":0.026263298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.67985374,"top":0.3810854,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.6903258,"top":0.3810854,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"synced_at","depth":8,"bounds":{"left":0.6509308,"top":0.39664805,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DATETIME","depth":8,"bounds":{"left":0.6771942,"top":0.39664805,"width":0.020944148,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.6981383,"top":0.39664805,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-- New event-driven capture columns","depth":8,"bounds":{"left":0.6509308,"top":0.41260973,"width":0.091921546,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"snapshot_path","depth":8,"bounds":{"left":0.65625,"top":0.42817238,"width":0.034075797,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.69298536,"top":0.42817238,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.70611703,"top":0.42817238,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.72706115,"top":0.42817238,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7375333,"top":0.42817238,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"accessibility_text","depth":8,"bounds":{"left":0.6509308,"top":0.4441341,"width":0.04720745,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.70079786,"top":0.4441341,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.71392953,"top":0.4441341,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7350399,"top":0.4441341,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.74551195,"top":0.4441341,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"accessibility_tree_json","depth":8,"bounds":{"left":0.6509308,"top":0.45969674,"width":0.060339097,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.71392953,"top":0.45969674,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.72706115,"top":0.45969674,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.74817157,"top":0.45969674,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7586436,"top":0.45969674,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"content_hash","depth":8,"bounds":{"left":0.6509308,"top":0.47525936,"width":0.03158245,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.6850067,"top":0.47525936,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.70611703,"top":0.47525936,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.72706115,"top":0.47525936,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7375333,"top":0.47525936,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"simhash","depth":8,"bounds":{"left":0.6509308,"top":0.49122107,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.671875,"top":0.49122107,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.69298536,"top":0.49122107,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.71392953,"top":0.49122107,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7244016,"top":0.49122107,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"capture_trigger","depth":8,"bounds":{"left":0.6509308,"top":0.5067837,"width":0.03939495,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.69298536,"top":0.5067837,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.70611703,"top":0.5067837,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.72706115,"top":0.5067837,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7375333,"top":0.5067837,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text_source","depth":8,"bounds":{"left":0.6509308,"top":0.5223464,"width":0.028922873,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6825133,"top":0.5223464,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.695645,"top":0.5223464,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7165891,"top":0.5223464,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.72706115,"top":0.5223464,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cloud_blob_id","depth":8,"bounds":{"left":0.6509308,"top":0.5383081,"width":0.034075797,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.68766624,"top":0.5383081,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.70079786,"top":0.5383081,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7219083,"top":0.5383081,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.73238033,"top":0.5383081,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"full_text","depth":8,"bounds":{"left":0.6509308,"top":0.55387074,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6771942,"top":0.55387074,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.6903258,"top":0.55387074,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.71127,"top":0.55387074,"width":0.010638298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7219083,"top":0.55387074,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"elements_ref_frame_id","depth":8,"bounds":{"left":0.6509308,"top":0.56943333,"width":0.05518617,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.7087766,"top":0.56943333,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.7297208,"top":0.56943333,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7506649,"top":0.56943333,"width":0.010638298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7613032,"top":0.56943333,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"document_path","depth":8,"bounds":{"left":0.6509308,"top":0.58539504,"width":0.034075797,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.68766624,"top":0.58539504,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.70079786,"top":0.58539504,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.7219083,"top":0.58539504,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.73238033,"top":0.58539504,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"accessibility_redacted_at","depth":8,"bounds":{"left":0.6509308,"top":0.6009577,"width":0.06565824,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.7244016,"top":0.6009577,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.7428524,"top":0.6009577,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"image_redacted_at","depth":8,"bounds":{"left":0.6509308,"top":0.6169194,"width":0.044714097,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.71392953,"top":0.6169194,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.73238033,"top":0.6169194,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FOREIGN","depth":8,"bounds":{"left":0.6509308,"top":0.63248205,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"KEY","depth":8,"bounds":{"left":0.671875,"top":0.63248205,"width":0.007978723,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":8,"bounds":{"left":0.6825133,"top":0.63248205,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"video_chunk_id","depth":8,"bounds":{"left":0.6850067,"top":0.63248205,"width":0.036901597,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":8,"bounds":{"left":0.7219083,"top":0.63248205,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"REFERENCES","depth":8,"bounds":{"left":0.72706115,"top":0.63248205,"width":0.026263298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"video_chunks","depth":8,"bounds":{"left":0.75598407,"top":0.63248205,"width":0.03158245,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":8,"bounds":{"left":0.7875665,"top":0.63248205,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":8,"bounds":{"left":0.79005986,"top":0.63248205,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":8,"bounds":{"left":0.795379,"top":0.63248205,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":8,"bounds":{"left":0.6456117,"top":0.6480447,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Columns","depth":7,"bounds":{"left":0.6456117,"top":0.67517954,"width":0.3494016,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Columns","depth":8,"bounds":{"left":0.6456117,"top":0.67517954,"width":0.032912236,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":10,"bounds":{"left":0.64960104,"top":0.7186752,"width":0.016788565,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Data type","depth":10,"bounds":{"left":0.77244014,"top":0.7186752,"width":0.021110373,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Allow null","depth":10,"bounds":{"left":0.8267952,"top":0.7186752,"width":0.021110373,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Primary key","depth":10,"bounds":{"left":0.88115025,"top":0.7186752,"width":0.025930852,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Actions","depth":10,"bounds":{"left":0.9444814,"top":0.7186752,"width":0.016456118,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":11,"bounds":{"left":0.64960104,"top":0.7565842,"width":0.004654255,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":11,"bounds":{"left":0.77244014,"top":0.7565842,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":0.754988,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.88115025,"top":0.754988,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.754988,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"video_chunk_id","depth":11,"bounds":{"left":0.64960104,"top":0.792498,"width":0.032413565,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":11,"bounds":{"left":0.77244014,"top":0.792498,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":0.79090184,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.79090184,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"offset_index","depth":11,"bounds":{"left":0.64960104,"top":0.8284118,"width":0.027759308,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":11,"bounds":{"left":0.77244014,"top":0.8284118,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.82681566,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":11,"bounds":{"left":0.64960104,"top":0.8639266,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":11,"bounds":{"left":0.77244014,"top":0.8639266,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.86233044,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"name","depth":11,"bounds":{"left":0.64960104,"top":0.89984035,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.77244014,"top":0.89984035,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":0.8982442,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.8982442,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app_name","depth":11,"bounds":{"left":0.64960104,"top":0.9353551,"width":0.01861702,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.77244014,"top":0.9353551,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":0.933759,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.933759,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"window_name","depth":11,"bounds":{"left":0.64960104,"top":0.97126895,"width":0.02543218,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.77244014,"top":0.97126895,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":0.9696728,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":0.9696728,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"focused","depth":11,"bounds":{"left":0.64960104,"top":1.0,"width":0.016289894,"height":-0.0071827173},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BOOLEAN","depth":11,"bounds":{"left":0.77244014,"top":1.0,"width":0.016289894,"height":-0.0071827173},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":1.0,"width":0.003656915,"height":-0.005586624},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":1.0,"width":0.021276595,"height":-0.005586624},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"browser_url","depth":11,"bounds":{"left":0.64960104,"top":1.0,"width":0.02543218,"height":-0.04269755},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.77244014,"top":1.0,"width":0.00930851,"height":-0.04269755},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8267952,"top":1.0,"width":0.003656915,"height":-0.041101336},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9444814,"top":1.0,"width":0.021276595,"height":-0.041101336},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"device_name","depth":11,"bounds":{"left":0.64960104,"top":1.0,"width":0.02543218,"height":-0.078611374},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.77244014,"top":1.0,"width":0.00930851,"height":-0.078611374},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-7247205089043000670
|
-7106762006163428629
|
visual_change
|
accessibility
|
NULL
|
New Tab
New Tab
Screenpipe — Archive
Screenpipe — New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
All docs · AFFiNE
All docs · AFFiNE
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
sqlite-web 0.7.2
sqlite-web 0.7.2
db.sqlite
db.sqlite
frames
Query
Query
table name...
_sqlx_migrations
_sqlx_migrations
audio_chunks
audio_chunks
audio_tags
audio_tags
audio_transcriptions
audio_transcriptions
audio_transcriptions_fts (v)
audio_transcriptions_fts
(v)
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
elements
elements
elements_fts (v)
elements_fts
(v)
elements_fts_config
elements_fts_config
elements_fts_data
elements_fts_data
elements_fts_idx
elements_fts_idx
frames
frames
frames_fts (v)
frames_fts
(v)
frames_fts_config
frames_fts_config
frames_fts_data
frames_fts_data
frames_fts_idx
frames_fts_idx
meetings
meetings
memories
memories
memories_fts (v)
memories_fts
(v)
memories_fts_config
memories_fts_config
memories_fts_data
memories_fts_data
memories_fts_idx
memories_fts_idx
ocr_text
ocr_text
pipe_executions
pipe_executions
pipe_scheduler_state
pipe_scheduler_state
secrets
secrets
speaker_embeddings
speaker_embeddings
speakers
speakers
sqlite_sequence
sqlite_sequence
sqlite_stat1
sqlite_stat1
sqlite_stat4
sqlite_stat4
tags
tags
ui_events
ui_events
ui_events_fts (v)
ui_events_fts
(v)
ui_events_fts_config
ui_events_fts_config
ui_events_fts_data
ui_events_fts_data
ui_events_fts_idx
ui_events_fts_idx
video_chunks
video_chunks
vision_tags
vision_tags
Toggle helper tables
Toggle helper tables
Structure
Structure
Content
Content
Query
Query
Export
Export
SQL
SQL
CREATE
TABLE
"frames"
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT
,
video_chunk_id
INTEGER
DEFAULT
NULL
,
offset_index
INTEGER
NOT
NULL
DEFAULT
0
,
timestamp
TIMESTAMP
NOT
NULL
,
name
TEXT
,
app_name
TEXT
DEFAULT
NULL
,
window_name
TEXT
DEFAULT
NULL
,
focused
BOOLEAN
DEFAULT
NULL
,
browser_url
TEXT
DEFAULT
NULL
,
device_name
TEXT
NOT
NULL
DEFAULT
''
,
sync_id
TEXT
,
machine_id
TEXT
,
synced_at
DATETIME
,
-- New event-driven capture columns
snapshot_path
TEXT
DEFAULT
NULL
,
accessibility_text
TEXT
DEFAULT
NULL
,
accessibility_tree_json
TEXT
DEFAULT
NULL
,
content_hash
INTEGER
DEFAULT
NULL
,
simhash
INTEGER
DEFAULT
NULL
,
capture_trigger
TEXT
DEFAULT
NULL
,
text_source
TEXT
DEFAULT
NULL
,
cloud_blob_id
TEXT
DEFAULT
NULL
,
full_text
TEXT
DEFAULT
NULL
,
elements_ref_frame_id
INTEGER
DEFAULT
NULL
,
document_path
TEXT
DEFAULT
NULL
,
accessibility_redacted_at
INTEGER
,
image_redacted_at
INTEGER
,
FOREIGN
KEY
(
video_chunk_id
)
REFERENCES
video_chunks
(
id
)
)
Columns
Columns
Column
Data type
Allow null
Primary key
Actions
id
INTEGER
✓
✓
read-only
video_chunk_id
INTEGER
✓
read-only
offset_index
INTEGER
read-only
timestamp
TIMESTAMP
read-only
name
TEXT
✓
read-only
app_name
TEXT
✓
read-only
window_name
TEXT
✓
read-only
focused
BOOLEAN
✓
read-only
browser_url
TEXT
✓
read-only
device_name
TEXT...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14699
|
654
|
16
|
2026-05-10T18:14:11.534352+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436851534_m2.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.5049867,"top":1.0,"width":0.008976064,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
visual_change
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14700
|
653
|
16
|
2026-05-10T18:14:14.317828+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436854317_m1.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":1.0,"height":0.9122222},"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.49027777,"top":0.033333335,"width":0.01875,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
visual_change
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14701
|
653
|
17
|
2026-05-10T18:14:44.486458+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436884486_m1.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.49027777,"top":0.033333335,"width":0.01875,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
visual_change
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
14700
|
NULL
|
NULL
|
NULL
|
|
14705
|
653
|
20
|
2026-05-10T18:14:56.972681+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436896972_m1.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.49027777,"top":0.033333335,"width":0.01875,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
click
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
14704
|
NULL
|
NULL
|
NULL
|
|
14706
|
654
|
18
|
2026-05-10T18:14:57.010402+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436897010_m2.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.5049867,"top":1.0,"width":0.008976064,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
click
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14707
|
653
|
21
|
2026-05-10T18:14:59.578690+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436899578_m1.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.49027777,"top":0.033333335,"width":0.01875,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
visual_change
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14708
|
654
|
19
|
2026-05-10T18:15:01.962801+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436901962_m2.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.5049867,"top":1.0,"width":0.008976064,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
click
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14715
|
653
|
25
|
2026-05-10T18:15:18.101033+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436918101_m1.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.49027777,"top":0.033333335,"width":0.01875,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
click
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
14716
|
654
|
23
|
2026-05-10T18:15:18.143406+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-10/1778 /Users/lukas/.screenpipe/data/data/2026-05-10/1778436918143_m2.jpg...
|
iTerm2
|
ssh
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","depth":4,"on_screen":true,"value":"D window_name IS NOT NULL\\\\n\\\" rows_affected=0 rows_returned \n riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\\n2026-05-07T21:50:34.470706Z INFO scre \n (hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf \n ety-net poll)\\n2026-05-07T21:50:34.479630Z INFO screenpipe_ \n tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream \n screenpipe install https://screenpi.pe/start.json s after unlock/wake for monitor 1\\n2026-05-07T21:50:34.49504 \n 0Z INFO sck_rs::stream_manager: stopped 1 persistent stream \n 2026-05-07T21:00:48.741587Z INFO screenpipe_engine::event_d (s)\\n2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manage \n riven_capture: content dedup: skipping capture for monitor 2 r: persistent SCK stream started for display 1 (1440x900, 2f \n (hash=2730807366803551277, trigger=click) ps, 2 excluded)\\n2026-05-07T21:50:37.714776Z INFO sck_rs::s \n 2026-05-07T21:00:55.766541Z INFO screenpipe_engine::event_d tream_manager: persistent SCK stream started for display 2 ( \n riven_capture: content dedup: skipping capture for monitor 1 3008x1253, 2fps, 2 excluded)\\nzsh: terminated npx screenpip \n (hash=2730807366803551277, trigger=click) e@latest record --disable-audio --ignored-windows \\\"Boostero \n 2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\\\"\\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\\ \n riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\\nwarning: parakeet is not suppo \n (hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\\n2026-05-0 \n 2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au \n riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\\nchecking permissions...\\n \n (hash=2730807366803551277, trigger=click) screen recording: ok\\n accessibility: ok\\n2026-05-08T09:2 \n 2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version \n t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\\n2026-05-08T09:25:45.851716Z \n 2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep \n t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\\n2026-05-08T09:25:45.853302Z INFO screenpipe_ \n (6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register \n 2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\\n2026-05-08T09:25:45.853909Z INFO \n t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w \n (6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback) \n \\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi \n tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr \n screenpipe login ue accessibility=true keychain=true\\n2026-05-08T09:25:45.874 \n 606Z INFO screenpipe: meeting detector enabled — independen \n 2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\\n2026-05-08T09:25:45.874891Z INFO s \n riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on 127.0.0.1:3030 (localhost \n (hash=2730807366803551277, trigger=click) only)\\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au \n 2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\\n2 \n riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m \n (hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\\n2026-05- \n 2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager: \n riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\\n2026-05-08T09:25:45.874854 \n (hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com \n 2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\\n2026-05-08 \n riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe: \n (hash=-3190010697038351257, trigger=click) day-recap\\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor \n 2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\\n2026-05-08T09:25:45.8 \n riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\\ \n (hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l \n 2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\\n2026-05-08T09:25:45.878921Z INF \n t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\\n2026-05 \n 2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\\n2026-05-08T09:25:45.879015Z INFO scree \n (5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \\\"/Users/lukas/.scree \n 2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\\\"\\n\\n\\n\\n \n t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \\n __________________ ___ ____ ____ \n 4.4x), 9 JPEGs deleted (_____ ___ \\n / ___/ ___/ ___/ _ \\\\/ _ \\\\/ __ \\\\ / __ \\ \n \\/ / __ \\\\/ _ \\\\\\n (__ / /__/ / / __/ __/ / / / / /_/ / \n tip: get the screenpipe desktop app for the full experienc / /_/ / __/\\n/____/\\\\___/_/ \\\\___/\\\\___/_/ /_/ / .___/_ \n e / .___/\\\\___/ \\n /_/ /_/ \n https://screenpi.pe \\n\\n\\n\\npower AI by everything you've seen, said \n or heard\\nopen source | runs locally | developer friendly\\n\\ \n 2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\\n┌────────────────────────┬─────────────────────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ─────┐\\n│ setting │ value \n (hash=2730807366803551277, trigger=click) │\\n├────────────────────────┼───────────────────── \n 2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\\n│ audio chunk duration │ 30 seconds \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ port │ 3030 \n (hash=-6936887013967671323, trigger=click) │\\n│ audio disabled │ true \n 2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\\n│ vision disabled │ \n riven_capture: content dedup: skipping capture for monitor 2 false │\\n│ pause on DRM content \n (hash=-6936887013967671323, trigger=click) │ false │\\n│ audio engine \n 2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\\n│ vad engine \n t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\\n│ data \n 2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\\n│ \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false \n (8.0x), 14 JPEGs deleted │\\n│ telemetry │ true \n 2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\\n│ use pii removal │ true \n t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\\n│ use all monitors │ true \n (4.4x), 13 JPEGs deleted │\\n2026-05-08T09:25:45.881529Z INFO screenpi \n 2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\\n│ ign \n riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\\\"Boosteroid\\\"] │ \n (hash=-6936887013967671323, trigger=click) \\n│ included windows │ [] \n │\\n│ cloud sync │ disabled \n tip: wire screenpipe into claude with one command: │\\n│ auto-destruct pid │ 0 \n claude mcp add screenpipe -- npx -y screenpipe-mcp │\\n│ deepgram key │ not set \n then ask claude to build a pipe that tracks who you a │\\n│ api auth │ enabled \n re, your todos, and how you spend your time from your screen │\\n│ encrypt secrets │ disabl \n activity ed │\\n│ retention days │ 1 \n 4 │\\n│ retention mode \n 2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\\n├───────────────── \n riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\\n│ languages \n (hash=6913338268963121281, trigger=click) │ │\\n│ \n 2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\\n├── \n riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤ \n (hash=6913338268963121281, trigger=click) \\n│ monitors │ \n 2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\\n│ │ id: 1 \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ id: 2 \n (hash=6913338268963121281, trigger=visual_change) │\\n├────────────────────────┼───────────────── \n 2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\\n│ audio devices │ \n riven_capture: content dedup: skipping capture for monitor 2 │\\n│ │ disabl \n (hash=6913338268963121281, trigger=visual_change) ed │\\n└────────────────────────┴── \n 2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\\nyou are using local pro \n riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\\n\\nwarning: t \n (hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\\nto \n 2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\\n\\ncheck latest \n riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea \n (hash=6913338268963121281, trigger=visual_change) ses\\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting \n 2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\\n2026-05-08T09:25:45.888762Z INFO screenpi \n riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc \n (hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\\n2026-05-08T09:25:45.89128 \n 2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found \n riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\\n2026-05-08T09:25:45.896583Z \n (hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu \n 2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_ \n riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8 \n (hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\\n2026-05-08T09:25:45.913483Z INFO screenpi \n 2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star \n riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\\n2026-05-08T09:25:45.913512Z INFO \n (hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin \n 2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2 \n t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\\n2026-05-08T09:25:45.914574Z INFO screenpi \n 2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star \n t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\\n2026-05-08T09:25:45.923 \n (4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12 \n 2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho 7.0.0.1:3030\\n2026-05-08T09:25:45.927056Z INFO screenpipe_c \n t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\\n202 \n (3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man \n ager::manager: Starting vision recording for monitor 1 (1440 \n tip: install a starter bundle of pipes: x900)\\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine:: \n screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m \n onitor 1 (device: monitor_1)\\n2026-05-08T09:25:46.311076Z I \n 2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca \n riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\\n2026-05-08T \n (hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma \n 2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\\n \n riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n (hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor \n 2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\\n2026-05-08T09:25:46.472981Z INFO scr \n riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start \n (hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\\n2026-05-08T09:25:46.472989Z INFO sc \n 2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m \n riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur \n (hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\\n2026-05-08T09:25:46.47301 \n 2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv \n riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\\n2026-0 \n (hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste \n 2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu \n riven_capture: content dedup: skipping capture for monitor 2 ded)\"} \n (hash=6913338268963121281, trigger=visual_change) \n 2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=6913338268963121281, trigger=click) \n 2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:28.410976Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:36.882207Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:37.074891Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.366576Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:48.495008Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 1 \n (hash=5795911359130326036, trigger=click) \n 2026-05-07T21:23:49.547721Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:01.423031Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:04.466413Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:24:56.787200Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 28 eligible frames \n 2026-05-07T21:24:57.817202Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB \n (7.7x), 14 JPEGs deleted \n 2026-05-07T21:24:59.081061Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 12 frames, 2.3MB → 0.7MB \n (3.3x), 12 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:27:02.818198Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:27:05.774711Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:28:30.450527Z INFO screenpipe_engine::event_d \n riven_capture: content dedup: skipping capture for monitor 2 \n (hash=5795911359130326036, trigger=visual_change) \n 2026-05-07T21:29:59.102962Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 25 eligible frames \n 2026-05-07T21:29:59.987011Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.6x), 10 JPEGs deleted \n 2026-05-07T21:30:01.159682Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 13 frames, 2.6MB → 0.9MB \n (2.7x), 13 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:35:01.331399Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 20 eligible frames \n 2026-05-07T21:35:02.457282Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.4x), 10 JPEGs deleted \n 2026-05-07T21:35:03.532280Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.8MB ( \n 2.1x), 8 JPEGs deleted \n \n tip: wire screenpipe into claude with one command: \n claude mcp add screenpipe -- npx -y screenpipe-mcp \n then ask claude to build a pipe that tracks who you a \n re, your todos, and how you spend your time from your screen \n activity \n \n 2026-05-07T21:39:18.435385Z INFO screenpipe_engine::sleep_m \n onitor: Screen locked (CGSession safety-net poll) \n 2026-05-07T21:39:34.938546Z INFO sck_rs::stream_manager: re \n creating stream for display 2 (resolution change) \n 2026-05-07T21:40:03.554507Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:40:04.411131Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (5.7x), 10 JPEGs deleted \n 2026-05-07T21:40:05.319315Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.6MB \n (3.5x), 10 JPEGs deleted \n \n tip: install a starter bundle of pipes: \n screenpipe install https://screenpi.pe/start.json \n \n 2026-05-07T21:45:05.336676Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 22 eligible frames \n 2026-05-07T21:45:06.184141Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB \n (6.0x), 10 JPEGs deleted \n 2026-05-07T21:45:06.960114Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 10 frames, 2.0MB → 0.3MB \n (6.4x), 10 JPEGs deleted \n \n tip: sign in for higher AI quotas + cloud sync: \n screenpipe login \n \n 2026-05-07T21:50:06.992344Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: found 18 eligible frames \n 2026-05-07T21:50:07.821021Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.7MB → 0.4MB ( \n 4.6x), 8 JPEGs deleted \n 2026-05-07T21:50:08.563068Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction: 8 frames, 1.6MB → 0.3MB ( \n 5.1x), 8 JPEGs deleted \n \n tip: get the screenpipe desktop app for the full experienc \n e \n https://screenpi.pe \n \n 2026-05-07T21:50:34.305285Z WARN sqlx::query: summary=\"SELE \n CT DISTINCT app_name, window_name, …\" db.statement=\"\\n\\nSELE \n CT\\n DISTINCT app_name,\\n window_name,\\n browser_url\\nFRO \n M\\n frames\\nWHERE\\n timestamp > datetime('now', '-30 secon \n ds')\\n AND app_name IS NOT NULL\\n AND window_name IS NOT N \n ULL\\n\" rows_affected=0 rows_returned=182 elapsed=1.9089s \n 2026-05-07T21:50:34.470706Z INFO screenpipe_engine::sleep_m \n onitor: Screen unlocked (CGSession safety-net poll) \n 2026-05-07T21:50:34.479630Z INFO screenpipe_engine::event_d \n riven_capture: invalidating persistent streams after unlock/ \n wake for monitor 1 \n 2026-05-07T21:50:34.495040Z INFO sck_rs::stream_manager: st \n opped 1 persistent stream(s) \n 2026-05-07T21:50:36.741433Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \n 2026-05-07T21:50:37.714776Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 2 (3008x1253, 2fps, \n 2 excluded) \n zsh: terminated npx screenpipe@latest record --disable-audi \n o --ignored-windows \"Boosteroid\" \n lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start \n detected hardware tier: Mid \n warning: parakeet is not supported on this platform, using w \n hisper-tiny instead \n 2026-05-08T09:25:44.404966Z INFO screenpipe_engine::auth_ke \n y: api auth: key resolved via secret store \n checking permissions... \n screen recording: ok \n accessibility: ok \n 2026-05-08T09:25:44.477216Z INFO screenpipe_screen::monitor \n ::macos_version: Detected macOS version: 14.6 \n 2026-05-08T09:25:45.851716Z INFO screenpipe_engine::sleep_m \n onitor: Starting macOS sleep/wake monitor \n 2026-05-08T09:25:45.853302Z INFO screenpipe_engine::sleep_m \n onitor: Screen lock/unlock observers registered (CFNotificat \n ionCenter) \n 2026-05-08T09:25:45.853909Z INFO screenpipe_engine::sleep_m \n onitor: Display reconfiguration watcher registered (CGDispla \n yRegisterReconfigurationCallback) \n 2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permiss \n ion_monitor: permission monitor started screen=true mic=true \n accessibility=true keychain=true \n 2026-05-08T09:25:45.874606Z INFO screenpipe: meeting detect \n or enabled — independent of transcription mode \n 2026-05-08T09:25:45.874891Z INFO screenpipe: API server lis \n tening on 127.0.0.1:3030 (localhost only) \n 2026-05-08T09:25:45.874915Z INFO screenpipe: API auth enabl \n ed — run `screenpipe auth token` to view your key \n 2026-05-08T09:25:45.874789Z INFO screenpipe_engine::power:: \n manager: power manager started (poll interval: 10s) \n 2026-05-08T09:25:45.874931Z INFO screenpipe_engine::vision_ \n manager::manager: Starting VisionManager \n 2026-05-08T09:25:45.874854Z INFO screenpipe_engine::snapsho \n t_compaction: snapshot compaction worker started (min_age=60 \n 0s, poll=300s) \n 2026-05-08T09:25:45.877906Z INFO screenpipe_core::pipes: lo \n aded pipe: day-recap \n 2026-05-08T09:25:45.878202Z INFO screenpipe_core::pipes: lo \n aded pipe: standup-update \n 2026-05-08T09:25:45.878737Z INFO screenpipe_core::pipes: lo \n aded pipe: ai-habits \n 2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: lo \n aded pipe: time-breakdown \n 2026-05-08T09:25:45.878921Z INFO screenpipe_core::pipes: lo \n aded pipe: video-export \n 2026-05-08T09:25:45.878997Z INFO screenpipe_core::pipes: lo \n aded pipe: meeting-summary \n 2026-05-08T09:25:45.879015Z INFO screenpipe_core::pipes: lo \n aded 6 pipes from \"/Users/lukas/.screenpipe/pipes\" \n \n \n \n _ \n __________________ ___ ____ ____ (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \\/ / __ \\/ _ \\ \n (__ / /__/ / / __/ __/ / / / / /_/ / / /_/ / __/ \n /____/\\___/_/ \\___/\\___/_/ /_/ / .___/_/ .___/\\___/ \n /_/ /_/ \n \n \n \n power AI by everything you've seen, said or heard \n open source | runs locally | developer friendly \n \n \n ┌────────────────────────┬────────────────────────────────── \n ──┐ \n │ setting │ value \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio chunk duration │ 30 seconds \n │ \n │ port │ 3030 \n │ \n │ audio disabled │ true \n │ \n │ vision disabled │ false \n │ \n │ pause on DRM content │ false \n │ \n │ audio engine │ Parakeet \n │ \n │ vad engine │ Silero \n │ \n │ data directory │ /Users/lukas/.screenpipe \n │ \n │ debug mode │ false \n │ \n │ telemetry │ true \n │ \n │ use pii removal │ true \n │ \n │ use all monitors │ true \n │ \n 2026-05-08T09:25:45.881529Z INFO screenpipe_core::pipes: pi \n pe scheduler started (generation 2) \n │ ignored windows │ [\"Boosteroid\"] \n │ \n │ included windows │ [] \n │ \n │ cloud sync │ disabled \n │ \n │ auto-destruct pid │ 0 \n │ \n │ deepgram key │ not set \n │ \n │ api auth │ enabled \n │ \n │ encrypt secrets │ disabled \n │ \n │ retention days │ 14 \n │ \n │ retention mode │ media-only (keep transcripts) \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ languages │ \n │ \n │ │ all languages \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ monitors │ \n │ \n │ │ id: 1 \n │ \n │ │ id: 2 \n │ \n ├────────────────────────┼────────────────────────────────── \n ──┤ \n │ audio devices │ \n │ \n │ │ disabled \n │ \n └────────────────────────┴────────────────────────────────── \n ──┘ \n you are using local processing. all your data stays on your \n computer. \n \n warning: telemetry is enabled. only error-level data will be \n sent. \n to disable, use the --disable-telemetry flag. \n \n check latest changes here: https://github.com/screenpipe/scr \n eenpipe/releases \n 2026-05-08T09:25:45.881961Z INFO screenpipe: starting UI ev \n ent capture \n 2026-05-08T09:25:45.888762Z INFO screenpipe_engine::power:: \n manager: initial power profile: Performance (on_ac=true, bat \n tery=Some(100)) \n 2026-05-08T09:25:45.891286Z WARN screenpipe: pi agent insta \n ll failed: bun not found — install from https://bun.sh \n 2026-05-08T09:25:45.896583Z INFO screenpipe_engine::ui_reco \n rder: Starting UI event capture \n 2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_reco \n rder: UI recording session started: 03da4156-9bc1-4c59-86f8- \n 3b937ccacca2 \n 2026-05-08T09:25:45.913483Z INFO screenpipe_engine::calenda \n r_speaker_id: speaker identification: started (user_name=<no \n t set>) \n 2026-05-08T09:25:45.913512Z INFO screenpipe_engine::hot_fra \n me_cache: hot_frame_cache: warming from DB (2026-05-07 06:25 \n :45.913511 UTC to 2026-05-08 06:25:45.913511 UTC) \n 2026-05-08T09:25:45.914574Z INFO screenpipe_engine::meeting \n _detector: meeting v2: detection loop started (base_interval \n =5s, profiles=12) \n 2026-05-08T09:25:45.923208Z INFO screenpipe_engine::server: \n Server listening on 127.0.0.1:3030 \n 2026-05-08T09:25:45.927056Z INFO screenpipe_connect::mdns: \n mdns: advertising screenpipe on port 3030 \n 2026-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 1 (1 \n 440x900) \n 2026-05-08T09:25:46.311039Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 1 (device: monitor_1) \n 2026-05-08T09:25:46.311076Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 1 (d \n evice: monitor_1) \n 2026-05-08T09:25:46.472936Z INFO screenpipe_engine::vision_ \n manager::manager: Starting vision recording for monitor 2 (3 \n 008x1253) \n 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_ \n manager::manager: Starting event-driven capture for monitor \n 2 (device: monitor_2) \n 2026-05-08T09:25:46.472981Z INFO screenpipe_engine::vision_ \n manager::manager: VisionManager started with 2/2 monitor(s) \n 2026-05-08T09:25:46.472989Z INFO screenpipe_engine::vision_ \n manager::monitor_watcher: Starting monitor watcher (event-dr \n iven via CGDisplayRegisterReconfigurationCallback, 60s backs \n top poll) \n 2026-05-08T09:25:46.473017Z INFO screenpipe_engine::event_d \n riven_capture: event-driven capture started for monitor 2 (d \n evice: monitor_2) \n 2026-05-08T09:25:47.579143Z INFO sck_rs::stream_manager: pe \n rsistent SCK stream started for display 1 (1440x900, 2fps, 2 \n excluded) \nAdm1n@DXP4800PLUS-B5F8:~$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.32912233,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33111703,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.3879654,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3899601,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.44680852,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4488032,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5056516,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50764626,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.56449467,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56648934,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ssh","depth":2,"bounds":{"left":0.62333775,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6253325,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6821808,"top":1.0,"width":0.058843084,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.68417555,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ssh","depth":1,"bounds":{"left":0.5049867,"top":1.0,"width":0.008976064,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7246826097812669706
|
5601208055224440121
|
click
|
accessibility
|
NULL
|
D window_name IS NOT NULL\\n\" rows_affected=0 D window_name IS NOT NULL\\n\" rows_affected=0 rows_returned
riven_capture: content dedup: skipping capture for monitor 2 =182 elapsed=1.9089s\n2026-05-07T21:50:34.470706Z INFO scre
(hash=2730807366803551277, trigger=click) enpipe_engine::sleep_monitor: Screen unlocked (CGSession saf
ety-net poll)\n2026-05-07T21:50:34.479630Z INFO screenpipe_
tip: install a starter bundle of pipes: engine::event_driven_capture: invalidating persistent stream
screenpipe install [URL_WITH_CREDENTIALS] record --disable-audio --ignored-windows \"Boostero
2026-05-07T21:01:01.987730Z INFO screenpipe_engine::event_d id\"\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sp-start\
riven_capture: content dedup: skipping capture for monitor 1 ndetected hardware tier: Mid\nwarning: parakeet is not suppo
(hash=2730807366803551277, trigger=click) rted on this platform, using whisper-tiny instead\n2026-05-0
2026-05-07T21:01:19.657174Z INFO screenpipe_engine::event_d 8T09:25:44.404966Z INFO screenpipe_engine::auth_key: api au
riven_capture: content dedup: skipping capture for monitor 2 th: key resolved via secret store\nchecking permissions...\n
(hash=2730807366803551277, trigger=click) screen recording: ok\n accessibility: ok\n2026-05-08T09:2
2026-05-07T21:04:45.665545Z INFO screenpipe_engine::snapsho 5:44.477216Z INFO screenpipe_screen::monitor::macos_version
t_compaction: snapshot compaction: found 24 eligible frames : Detected macOS version: 14.6\n2026-05-08T09:25:45.851716Z
2026-05-07T21:04:46.555243Z INFO screenpipe_engine::snapsho INFO screenpipe_engine::sleep_monitor: Starting macOS sleep
t_compaction: snapshot compaction: 11 frames, 2.4MB → 0.4MB /wake monitor\n2026-05-08T09:25:45.853302Z INFO screenpipe_
(6.5x), 11 JPEGs deleted engine::sleep_monitor: Screen lock/unlock observers register
2026-05-07T21:04:47.505518Z INFO screenpipe_engine::snapsho ed (CFNotificationCenter)\n2026-05-08T09:25:45.853909Z INFO
t_compaction: snapshot compaction: 11 frames, 2.1MB → 0.3MB screenpipe_engine::sleep_monitor: Display reconfiguration w
(6.3x), 11 JPEGs deleted atcher registered (CGDisplayRegisterReconfigurationCallback)
\n2026-05-08T09:25:45.874540Z INFO screenpipe_engine::permi
tip: sign in for higher AI quotas + cloud sync: ssion_monitor: permission monitor started screen=true mic=tr
screenpipe login ue accessibility=true keychain=true\n2026-05-08T09:25:45.874
606Z INFO screenpipe: meeting detector enabled — independen
2026-05-07T21:07:59.390425Z INFO screenpipe_engine::event_d t of transcription mode\n2026-05-08T09:25:45.874891Z INFO s
riven_capture: content dedup: skipping capture for monitor 1 creenpipe: API server listening on [IP_ADDRESS]:3030 (localhost
(hash=2730807366803551277, trigger=click) only)\n2026-05-08T09:25:45.874915Z INFO screenpipe: API au
2026-05-07T21:08:02.049042Z INFO screenpipe_engine::event_d th enabled — run `screenpipe auth token` to view your key\n2
riven_capture: content dedup: skipping capture for monitor 1 026-05-08T09:25:45.874789Z INFO screenpipe_engine::power::m
(hash=2730807366803551277, trigger=click) anager: power manager started (poll interval: 10s)\n2026-05-
2026-05-07T21:08:58.406336Z INFO screenpipe_engine::event_d 08T09:25:45.874931Z INFO screenpipe_engine::vision_manager:
riven_capture: content dedup: skipping capture for monitor 1 :manager: Starting VisionManager\n2026-05-08T09:25:45.874854
(hash=-3190010697038351257, trigger=click) Z INFO screenpipe_engine::snapshot_compaction: snapshot com
2026-05-07T21:08:58.638209Z INFO screenpipe_engine::event_d paction worker started (min_age=600s, poll=300s)\n2026-05-08
riven_capture: content dedup: skipping capture for monitor 2 T09:25:45.877906Z INFO screenpipe_core::pipes: loaded pipe:
(hash=-3190010697038351257, trigger=click) day-recap\n2026-05-08T09:25:45.878202Z INFO screenpipe_cor
2026-05-07T21:09:33.105308Z INFO screenpipe_engine::event_d e::pipes: loaded pipe: standup-update\n2026-05-08T09:25:45.8
riven_capture: content dedup: skipping capture for monitor 2 78737Z INFO screenpipe_core::pipes: loaded pipe: ai-habits\
(hash=2730807366803551277, trigger=click) n2026-05-08T09:25:45.878843Z INFO screenpipe_core::pipes: l
2026-05-07T21:09:47.541287Z INFO screenpipe_engine::snapsho oaded pipe: time-breakdown\n2026-05-08T09:25:45.878921Z INF
t_compaction: snapshot compaction: found 21 eligible frames O screenpipe_core::pipes: loaded pipe: video-export\n2026-05
2026-05-07T21:09:48.491343Z INFO screenpipe_engine::snapsho -08T09:25:45.878997Z INFO screenpipe_core::pipes: loaded pi
t_compaction: snapshot compaction: 10 frames, 2.2MB → 0.4MB pe: meeting-summary\n2026-05-08T09:25:45.879015Z INFO scree
(5.6x), 10 JPEGs deleted npipe_core::pipes: loaded 6 pipes from \"/Users/lukas/.scree
2026-05-07T21:09:49.574479Z INFO screenpipe_engine::snapsho npipe/pipes\"\n\n\n\n
t_compaction: snapshot compaction: 9 frames, 1.8MB → 0.4MB ( _ \n __________________ ___ ____ ____
4.4x), 9 JPEGs deleted (_____ ___ \n / ___/ ___/ ___/ _ \\/ _ \\/ __ \\ / __ \
\/ / __ \\/ _ \\\n (__ / /__/ / / __/ __/ / / / / /_/ /
tip: get the screenpipe desktop app for the full experienc / /_/ / __/\n/____/\\___/_/ \\___/\\___/_/ /_/ / .___/_
e / .___/\\___/ \n /_/ /_/
https://screenpi.pe \n\n\n\npower AI by everything you've seen, said
or heard\nopen source | runs locally | developer friendly\n\
2026-05-07T21:11:18.711785Z INFO screenpipe_engine::event_d n\n┌────────────────────────┬───────────────────────────────
riven_capture: content dedup: skipping capture for monitor 2 ─────┐\n│ setting │ value
(hash=2730807366803551277, trigger=click) │\n├────────────────────────┼─────────────────────
2026-05-07T21:11:43.731627Z INFO screenpipe_engine::event_d ───────────────┤\n│ audio chunk duration │ 30 seconds
riven_capture: content dedup: skipping capture for monitor 2 │\n│ port │ 3030
(hash=-6936887013967671323, trigger=click) │\n│ audio disabled │ true
2026-05-07T21:11:58.569054Z INFO screenpipe_engine::event_d │\n│ vision disabled │
riven_capture: content dedup: skipping capture for monitor 2 false │\n│ pause on DRM content
(hash=-6936887013967671323, trigger=click) │ false │\n│ audio engine
2026-05-07T21:14:49.597611Z INFO screenpipe_engine::snapsho │ Parakeet │\n│ vad engine
t_compaction: snapshot compaction: found 29 eligible frames │ Silero │\n│ data
2026-05-07T21:14:50.698785Z INFO screenpipe_engine::snapsho directory │ /Users/lukas/.screenpipe │\n│
t_compaction: snapshot compaction: 14 frames, 3.1MB → 0.4MB debug mode │ false
(8.0x), 14 JPEGs deleted │\n│ telemetry │ true
2026-05-07T21:14:53.637486Z INFO screenpipe_engine::snapsho │\n│ use pii removal │ true
t_compaction: snapshot compaction: 13 frames, 2.5MB → 0.6MB │\n│ use all monitors │ true
(4.4x), 13 JPEGs deleted │\n2026-05-08T09:25:45.881529Z INFO screenpi
2026-05-07T21:15:02.521080Z INFO screenpipe_engine::event_d pe_core::pipes: pipe scheduler started (generation 2)\n│ ign
riven_capture: content dedup: skipping capture for monitor 1 ored windows │ [\"Boosteroid\"] │
(hash=-6936887013967671323, trigger=click) \n│ included windows │ []
│\n│ cloud sync │ disabled
tip: wire screenpipe into claude with one command: │\n│ auto-destruct pid │ 0
claude mcp add screenpipe -- npx -y screenpipe-mcp │\n│ deepgram key │ not set
then ask claude to build a pipe that tracks who you a │\n│ api auth │ enabled
re, your todos, and how you spend your time from your screen │\n│ encrypt secrets │ disabl
activity ed │\n│ retention days │ 1
4 │\n│ retention mode
2026-05-07T21:15:56.220866Z INFO screenpipe_engine::event_d │ media-only (keep transcripts) │\n├─────────────────
riven_capture: content dedup: skipping capture for monitor 2 ───────┼────────────────────────────────────┤\n│ languages
(hash=6913338268963121281, trigger=click) │ │\n│
2026-05-07T21:16:19.271233Z INFO screenpipe_engine::event_d │ all languages │\n├──
riven_capture: content dedup: skipping capture for monitor 1 ──────────────────────┼────────────────────────────────────┤
(hash=6913338268963121281, trigger=click) \n│ monitors │
2026-05-07T21:17:24.638614Z INFO screenpipe_engine::event_d │\n│ │ id: 1
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ id: 2
(hash=6913338268963121281, trigger=visual_change) │\n├────────────────────────┼─────────────────
2026-05-07T21:17:30.785805Z INFO screenpipe_engine::event_d ───────────────────┤\n│ audio devices │
riven_capture: content dedup: skipping capture for monitor 2 │\n│ │ disabl
(hash=6913338268963121281, trigger=visual_change) ed │\n└────────────────────────┴──
2026-05-07T21:17:33.819916Z INFO screenpipe_engine::event_d ──────────────────────────────────┘\nyou are using local pro
riven_capture: content dedup: skipping capture for monitor 2 cessing. all your data stays on your computer.\n\nwarning: t
(hash=6913338268963121281, trigger=visual_change) elemetry is enabled. only error-level data will be sent.\nto
2026-05-07T21:18:40.291660Z INFO screenpipe_engine::event_d disable, use the --disable-telemetry flag.\n\ncheck latest
riven_capture: content dedup: skipping capture for monitor 2 changes here: https://github.com/screenpipe/screenpipe/relea
(hash=6913338268963121281, trigger=visual_change) ses\n2026-05-08T09:25:45.881961Z INFO screenpipe: starting
2026-05-07T21:18:44.647424Z INFO screenpipe_engine::event_d UI event capture\n2026-05-08T09:25:45.888762Z INFO screenpi
riven_capture: content dedup: skipping capture for monitor 2 pe_engine::power::manager: initial power profile: Performanc
(hash=6913338268963121281, trigger=click) e (on_ac=true, battery=Some(100))\n2026-05-08T09:25:45.89128
2026-05-07T21:18:44.850725Z INFO screenpipe_engine::event_d 6Z WARN screenpipe: pi agent install failed: bun not found
riven_capture: content dedup: skipping capture for monitor 1 — install from https://bun.sh\n2026-05-08T09:25:45.896583Z
(hash=6913338268963121281, trigger=click) INFO screenpipe_engine::ui_recorder: Starting UI event captu
2026-05-07T21:18:46.523321Z INFO screenpipe_engine::event_d re\n2026-05-08T09:25:45.913159Z INFO screenpipe_engine::ui_
riven_capture: content dedup: skipping capture for monitor 2 recorder: UI recording session started: 03da4156-9bc1-4c59-8
(hash=6913338268963121281, trigger=visual_change) 6f8-3b937ccacca2\n2026-05-08T09:25:45.913483Z INFO screenpi
2026-05-07T21:18:49.556444Z INFO screenpipe_engine::event_d pe_engine::calendar_speaker_id: speaker identification: star
riven_capture: content dedup: skipping capture for monitor 2 ted (user_name=<not set>)\n2026-05-08T09:25:45.913512Z INFO
(hash=6913338268963121281, trigger=visual_change) screenpipe_engine::hot_frame_cache: hot_frame_cache: warmin
2026-05-07T21:19:53.705827Z INFO screenpipe_engine::snapsho g from DB (2026-05-07 06:25:45.913511 UTC to 2026-05-08 06:2
t_compaction: snapshot compaction: found 42 eligible frames 5:45.913511 UTC)\n2026-05-08T09:25:45.914574Z INFO screenpi
2026-05-07T21:19:55.056907Z INFO screenpipe_engine::snapsho pe_engine::meeting_detector: meeting v2: detection loop star
t_compaction: snapshot compaction: 20 frames, 3.6MB → 0.9MB ted (base_interval=5s, profiles=12)\n2026-05-08T09:25:45.923
(4.0x), 20 JPEGs deleted 208Z INFO screenpipe_engine::server: Server listening on 12
2026-05-07T21:19:56.759790Z INFO screenpipe_engine::snapsho [IP_ADDRESS]:3030\n2026-05-08T09:25:45.927056Z INFO screenpipe_c
t_compaction: snapshot compaction: 20 frames, 3.9MB → 1.1MB onnect::mdns: mdns: advertising screenpipe on port 3030\n202
(3.4x), 20 JPEGs deleted 6-05-08T09:25:46.310992Z INFO screenpipe_engine::vision_man
ager::manager: Starting vision recording for monitor 1 (1440
tip: install a starter bundle of pipes: x900)\n2026-05-08T09:25:46.311039Z INFO screenpipe_engine::
screenpipe install https://screenpi.pe/start.json vision_manager::manager: Starting event-driven capture for m
onitor 1 (device: monitor_1)\n2026-05-08T09:25:46.311076Z I
2026-05-07T21:20:29.459260Z INFO screenpipe_engine::event_d NFO screenpipe_engine::event_driven_capture: event-driven ca
riven_capture: content dedup: skipping capture for monitor 2 pture started for monitor 1 (device: monitor_1)\n2026-05-08T
(hash=6913338268963121281, trigger=visual_change) 09:25:46.472936Z INFO screenpipe_engine::vision_manager::ma
2026-05-07T21:20:41.532773Z INFO screenpipe_engine::event_d nager: Starting vision recording for monitor 2 (3008x1253)\n
riven_capture: content dedup: skipping capture for monitor 2 2026-05-08T09:25:46.472968Z INFO screenpipe_engine::vision_
(hash=6913338268963121281, trigger=visual_change) manager::manager: Starting event-driven capture for monitor
2026-05-07T21:20:57.170225Z INFO screenpipe_engine::event_d 2 (device: monitor_2)\n2026-05-08T09:25:46.472981Z INFO scr
riven_capture: content dedup: skipping capture for monitor 1 eenpipe_engine::vision_manager::manager: VisionManager start
(hash=6913338268963121281, trigger=click) ed with 2/2 monitor(s)\n2026-05-08T09:25:46.472989Z INFO sc
2026-05-07T21:21:07.150037Z INFO screenpipe_engine::event_d reenpipe_engine::vision_manager::monitor_watcher: Starting m
riven_capture: content dedup: skipping capture for monitor 1 onitor watcher (event-driven via CGDisplayRegisterReconfigur
(hash=6913338268963121281, trigger=click) ationCallback, 60s backstop poll)\n2026-05-08T09:25:46.47301
2026-05-07T21:21:07.320528Z INFO screenpipe_engine::event_d 7Z INFO screenpipe_engine::event_driven_capture: event-driv
riven_capture: content dedup: skipping capture for monitor 2 en capture started for monitor 2 (device: monitor_2)\n2026-0
(hash=6913338268963121281, trigger=click) 5-08T09:25:47.579143Z INFO sck_rs::stream_manager: persiste
2026-05-07T21:21:27.422746Z INFO screenpipe_engine::event_d nt SCK stream started for display 1 (1440x900, 2fps, 2 exclu
riven_capture: content dedup: skipping capture for monitor 2 ded)"}
(hash=6913338268963121281, trigger=visual_change)
2026-05-07T21:22:21.936188Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:22.033584Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=6913338268963121281, trigger=click)
2026-05-07T21:22:50.292905Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.735984Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:16.914206Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 2
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.420221Z INFO screenpipe_engine::event_d
riven_capture: content dedup: skipping capture for monitor 1
(hash=5795911359130326036, trigger=click)
2026-05-07T21:23:18.695513Z INFO screenpipe_engine::event_d ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
41
|
3
|
8
|
2026-05-06T17:31:04.406416+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-06/1778 /Users/lukas/.screenpipe/data/data/2026-05-06/1778088664406_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Firefox FileEditViewHistoryBookmarksProfilesToolsW Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelp<G-zsh95% <428•Wed 6 May 20:31:05L881DOCKER0 81DEV (docker)₴82"SNAS_DATA/STARGET_DATE/"2>>"SLOG_FILE"RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))COPIED_FILES=$(ls "SNAS_DATA/STARGET_DATE" | wc -1 | tr -d ' ')SRC_FILES=$(ls "SDATA_SRC" I wc -l l tr -d 'COPIED_SIZE=$(du -sh "SNAS_DATA/STARGET_DATE" | cut -f1)if [ "SCOPIED_FILES" -eq "$SRC_FILES" ]; thenprintf "\r%-36s / %dm%02ds(%s files, %s)\n" \"rsync STARGET_DATE/ → NAS" \"'S(( RSYNC_DUR / 60 ))" "'$(( RSYNC_DUR % 60 ))"\"SCOPIED_FILES" "SCOPIED_SIZE" | tee -a "SLOG_FILE"elseprintf "\rAPP (-zsh)%-36s x %s/ %s files\n""rsync STARGET_DATE/ → NAS" "SCOPIED_FILES" "SSRC_FILES" | tee -a "SLOG_FILE"fielseprintf "fi%-36s %s\n" "rsync STARGET_DATE/ » NAS" "skipped (no source dir)"# — SUMMARYTOTAL_ELAPSED=S( S(date +%5) - SCRIPT_START ))DB_SIZE=S(du -sh "SNAS_DB" | cut -f1)echo"*1og"Archive DBsize:1og"Total time:SDB_SIZE"S(( TOTAL_ELAPSED / 60 ))mS(( TOTAL_ELAPSED % 60 ))s"109"Sync complete for STARGET_DATE"10g"=lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ 11totaldrwxr-xr-x2 lukasstaff646 May 20:22drwx-94lukasstaff30086 May 20:27lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ cd ..lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cd ~/.screenpipelukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/.screenpipe $ 1ltotal 9424drwxr-xr-xdrwx-drwxr-xr-x-rw-r-rw-r-drwxr-xr-x-rw-r--r--8 lukas94 lukas5 lukas1 lukas1 lukas1lukas8 lukas1 lukasstaff2566 May 20:27staff30086 May 20:27staff1606 May 20:28datastaffstaff5816326 May 20:27db.sqlite327686 May20:27db.sqlite-shmstaffstaff33125126 May20:28db.sqlite-wal2566 May 20:27 pipesstaff95666 May 20:28 screenpipe.2026-05-06.0.10glukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ l*3-zsh®84screenpipe*• ₴5-zsh...
|
NULL
|
-7244784211529518441
|
NULL
|
click
|
ocr
|
NULL
|
Firefox FileEditViewHistoryBookmarksProfilesToolsW Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelp<G-zsh95% <428•Wed 6 May 20:31:05L881DOCKER0 81DEV (docker)₴82"SNAS_DATA/STARGET_DATE/"2>>"SLOG_FILE"RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))COPIED_FILES=$(ls "SNAS_DATA/STARGET_DATE" | wc -1 | tr -d ' ')SRC_FILES=$(ls "SDATA_SRC" I wc -l l tr -d 'COPIED_SIZE=$(du -sh "SNAS_DATA/STARGET_DATE" | cut -f1)if [ "SCOPIED_FILES" -eq "$SRC_FILES" ]; thenprintf "\r%-36s / %dm%02ds(%s files, %s)\n" \"rsync STARGET_DATE/ → NAS" \"'S(( RSYNC_DUR / 60 ))" "'$(( RSYNC_DUR % 60 ))"\"SCOPIED_FILES" "SCOPIED_SIZE" | tee -a "SLOG_FILE"elseprintf "\rAPP (-zsh)%-36s x %s/ %s files\n""rsync STARGET_DATE/ → NAS" "SCOPIED_FILES" "SSRC_FILES" | tee -a "SLOG_FILE"fielseprintf "fi%-36s %s\n" "rsync STARGET_DATE/ » NAS" "skipped (no source dir)"# — SUMMARYTOTAL_ELAPSED=S( S(date +%5) - SCRIPT_START ))DB_SIZE=S(du -sh "SNAS_DB" | cut -f1)echo"*1og"Archive DBsize:1og"Total time:SDB_SIZE"S(( TOTAL_ELAPSED / 60 ))mS(( TOTAL_ELAPSED % 60 ))s"109"Sync complete for STARGET_DATE"10g"=lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ 11totaldrwxr-xr-x2 lukasstaff646 May 20:22drwx-94lukasstaff30086 May 20:27lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ cd ..lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cd ~/.screenpipelukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/.screenpipe $ 1ltotal 9424drwxr-xr-xdrwx-drwxr-xr-x-rw-r-rw-r-drwxr-xr-x-rw-r--r--8 lukas94 lukas5 lukas1 lukas1 lukas1lukas8 lukas1 lukasstaff2566 May 20:27staff30086 May 20:27staff1606 May 20:28datastaffstaff5816326 May 20:27db.sqlite327686 May20:27db.sqlite-shmstaffstaff33125126 May20:28db.sqlite-wal2566 May 20:27 pipesstaff95666 May 20:28 screenpipe.2026-05-06.0.10glukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ l*3-zsh®84screenpipe*• ₴5-zsh...
|
39
|
NULL
|
NULL
|
NULL
|
|
15068
|
674
|
4
|
2026-05-11T06:14:28.157912+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778480068157_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmObjectsResolver.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelplalol| Daily - Platform • in 31 mDEV (docker)DOCKER→- *1DEV (docker)182APP (-zsh)|• *3masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY]@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny#-zsh84-zsh885100% <8• Mon 11 May 9:14:28T81screenpipe"O ₴6DEV...
|
NULL
|
-7243323742141231026
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelplalol| Daily - Platform • in 31 mDEV (docker)DOCKER→- *1DEV (docker)182APP (-zsh)|• *3masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY]@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny#-zsh84-zsh885100% <8• Mon 11 May 9:14:28T81screenpipe"O ₴6DEV...
|
15067
|
NULL
|
NULL
|
NULL
|
|
7930
|
352
|
9
|
2026-05-08T09:40:26.586014+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778233226586_m2.jpg...
|
Slack
|
releases (Channel) - Jiminny Inc - 4 new items - S releases (Channel) - Jiminny Inc - 4 new items - Slack...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivitySlackcalVIewMistonWindowHelp@ Describe wha ActivitySlackcalVIewMistonWindowHelp@ Describe what you are looking forJiminny ... ~# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesP. Nikolay IvanovP. Galya DimitrovaB Aneliya Angelova,...8 Stoyan Tanev •A. Stefka StoyanovaVes CR. Aneliya Angelova€. Vasil Vasilevdo James GrahamLukas Kovalik y... O#:AppsJira Cloud® Toast# releases8 22MessagesQ Files& BookmarksDeploymentToday~Project:When:app05/08/2026 07:16:21lag.View JobCircleCI APP 12:37 PMNew commits deployed to Prophet Prod-[7eed66c)[URL_WITH_CREDENTIALS] out mastervolocalhostllfiiminnv@localhdSF [jiminny@localhost]& zoho_dev jiminny@localh 122~ A PRODAconcole PROniI& console_1 [PROD]A ni rpponIl40 09 >PoST lanilvilautomated-renorts/interest1usaaepublic function trackInterest(Request $reque* @throws ApplicationException58 (g>GET /api/v1/automated-reportspublic function list(Request $request): Json123 (g>DELETE /api/vl/automated-reportskuuidpublic function delete(Request $request, str•• 0SevenShores|Hubspot\ExceptionsService-Desk - Queues - PlatformJy 20807 check various issues wiIlluminate|Queue\MaxAttemptsExcJY-20818 move ask jiminny report1 Useroilot 1 Ask liminny Report GenJY-20773 fix user pilot tracking ofProblem loadina paqeN Search the CRM - HubSpot docs8 Jiminny) New TabNew TabDashboards | DatadogAl Features | DatadogJy 20493 smart instant nudge pre~ Google Geminithe review. These optimizations tocus onreducing memory footprint, preventing lazy-loading, and ensuring data type resilience.….rix the n+ Querv Risk (zacer Loaclino))Your checkCategoryId and checkStageIdmethods call Sactivity->getCategory() and$activity->getStage() . If theserelationships aren't eager-loaded, Laravel willexecute a new database query for every checkinside the loop.Suggestion: Add category and stage to theinitial eager-load array.// Before (Line 38)Sactivity = Activity::with(['user.groupSactivity = Activity: withct'user.group2. Drastically Reduce Memory UsageCurrently, you are hydrating full Eloquent Usermodels into memory just to extract their IDs. Ifa team has hundreds or thousands of users,this will spike memory usage and potentiallycrash the worker.Suggestion: Use the relationship query builderto perform the pluck directly at the databaselevel. bvnassina Floauent model hudratior© Enter a prompt for GeminiAl. It can make mistakes, so double check it. Your privacy & GeminSummarize pageO JIMINNYg For you|@ Recent# Starred0+ AppsQ SpacesJiminny (New)lll Plauorm eamII Capture TeamŒ Enterprise Stability I...IM Processing TeamI SE Kanban( Service-Desk= More spaces= FiltersDashboardsC÷ Operations& Confiuence88 Teams|U= Customise sidebar50 lilSupport Daily - in 2h 20 m100% Lz8 Fri 8 May 12:40:27Q pipedrive+ CreateAsk RovoSpaces / Jiminny (New)vatrorm leam !Summarv—Timeline# BacklogI Active sprintsE Calendar L Reports Testing BoardHistÈ Forms e ComponentsMore 9+• Search board ITypevQuick filters vComplete sprintGroup: QueriesREADY FOR DEV 2IN DEV 3CODE REVIEW 2BLOCKEDQA 1PO ACCEPTANCEDEPLOY 7AJ Panorama forCall Scoring in ODAUTOMATED AI SCORINGReady for DevlAI Review - Q1 -summary Actionems/Key PointsGROWTH - MAINTAIN O..Smart Instantnuage Pre"filteringCOST-EFFECTIVE AND F..Sync opportunitieswitnout a localowner (user_id i.PLATFORM STABILITYAI Reports>Empty pagedesign and...AJ REPORTSCode ReviewIn @AIZ.5 •eee=Deployedna ee00 -1.5 88 =3 170000=1 đ •00=I.JY-2036][ JY-20566N JY-Z0495© JY-20352DJY-20372Setup testcoveraae toIProphet in SonarIPoC JiminnyMCP ConnectorMAINTENANCEJIMINNY MCP CONNEC…Move AsK Jiminny ...reports toseparated….BackloaIn ProgressCode ReviewGrok via AzureMAINTENANCEDeployed1• •=1•eee—10 0000 —NJY-20726E JY-19951Q JY-20625SEJY-208181lHubSootiIOptimise CRMrematching on…PLATFORM STABILITYAllow users todelete ss andPanorama...AJ REPORTSDeployedIn Devl1 72 •000=#* JY-20770*E JY-20725Release AJIPanorama reportsto customersAJ REPORTSDeoloved0.5đ•000=I JY-20740Wrong formattingfor summary in theCRMMAINTENANCEDeploved |...
|
NULL
|
-7242898537437970438
|
NULL
|
app_switch
|
ocr
|
NULL
|
ActivitySlackcalVIewMistonWindowHelp@ Describe wha ActivitySlackcalVIewMistonWindowHelp@ Describe what you are looking forJiminny ... ~# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesP. Nikolay IvanovP. Galya DimitrovaB Aneliya Angelova,...8 Stoyan Tanev •A. Stefka StoyanovaVes CR. Aneliya Angelova€. Vasil Vasilevdo James GrahamLukas Kovalik y... O#:AppsJira Cloud® Toast# releases8 22MessagesQ Files& BookmarksDeploymentToday~Project:When:app05/08/2026 07:16:21lag.View JobCircleCI APP 12:37 PMNew commits deployed to Prophet Prod-[7eed66c)[URL_WITH_CREDENTIALS] out mastervolocalhostllfiiminnv@localhdSF [jiminny@localhost]& zoho_dev jiminny@localh 122~ A PRODAconcole PROniI& console_1 [PROD]A ni rpponIl40 09 >PoST lanilvilautomated-renorts/interest1usaaepublic function trackInterest(Request $reque* @throws ApplicationException58 (g>GET /api/v1/automated-reportspublic function list(Request $request): Json123 (g>DELETE /api/vl/automated-reportskuuidpublic function delete(Request $request, str•• 0SevenShores|Hubspot\ExceptionsService-Desk - Queues - PlatformJy 20807 check various issues wiIlluminate|Queue\MaxAttemptsExcJY-20818 move ask jiminny report1 Useroilot 1 Ask liminny Report GenJY-20773 fix user pilot tracking ofProblem loadina paqeN Search the CRM - HubSpot docs8 Jiminny) New TabNew TabDashboards | DatadogAl Features | DatadogJy 20493 smart instant nudge pre~ Google Geminithe review. These optimizations tocus onreducing memory footprint, preventing lazy-loading, and ensuring data type resilience.….rix the n+ Querv Risk (zacer Loaclino))Your checkCategoryId and checkStageIdmethods call Sactivity->getCategory() and$activity->getStage() . If theserelationships aren't eager-loaded, Laravel willexecute a new database query for every checkinside the loop.Suggestion: Add category and stage to theinitial eager-load array.// Before (Line 38)Sactivity = Activity::with(['user.groupSactivity = Activity: withct'user.group2. Drastically Reduce Memory UsageCurrently, you are hydrating full Eloquent Usermodels into memory just to extract their IDs. Ifa team has hundreds or thousands of users,this will spike memory usage and potentiallycrash the worker.Suggestion: Use the relationship query builderto perform the pluck directly at the databaselevel. bvnassina Floauent model hudratior© Enter a prompt for GeminiAl. It can make mistakes, so double check it. Your privacy & GeminSummarize pageO JIMINNYg For you|@ Recent# Starred0+ AppsQ SpacesJiminny (New)lll Plauorm eamII Capture TeamŒ Enterprise Stability I...IM Processing TeamI SE Kanban( Service-Desk= More spaces= FiltersDashboardsC÷ Operations& Confiuence88 Teams|U= Customise sidebar50 lilSupport Daily - in 2h 20 m100% Lz8 Fri 8 May 12:40:27Q pipedrive+ CreateAsk RovoSpaces / Jiminny (New)vatrorm leam !Summarv—Timeline# BacklogI Active sprintsE Calendar L Reports Testing BoardHistÈ Forms e ComponentsMore 9+• Search board ITypevQuick filters vComplete sprintGroup: QueriesREADY FOR DEV 2IN DEV 3CODE REVIEW 2BLOCKEDQA 1PO ACCEPTANCEDEPLOY 7AJ Panorama forCall Scoring in ODAUTOMATED AI SCORINGReady for DevlAI Review - Q1 -summary Actionems/Key PointsGROWTH - MAINTAIN O..Smart Instantnuage Pre"filteringCOST-EFFECTIVE AND F..Sync opportunitieswitnout a localowner (user_id i.PLATFORM STABILITYAI Reports>Empty pagedesign and...AJ REPORTSCode ReviewIn @AIZ.5 •eee=Deployedna ee00 -1.5 88 =3 170000=1 đ •00=I.JY-2036][ JY-20566N JY-Z0495© JY-20352DJY-20372Setup testcoveraae toIProphet in SonarIPoC JiminnyMCP ConnectorMAINTENANCEJIMINNY MCP CONNEC…Move AsK Jiminny ...reports toseparated….BackloaIn ProgressCode ReviewGrok via AzureMAINTENANCEDeployed1• •=1•eee—10 0000 —NJY-20726E JY-19951Q JY-20625SEJY-208181lHubSootiIOptimise CRMrematching on…PLATFORM STABILITYAllow users todelete ss andPanorama...AJ REPORTSDeployedIn Devl1 72 •000=#* JY-20770*E JY-20725Release AJIPanorama reportsto customersAJ REPORTSDeoloved0.5đ•000=I JY-20740Wrong formattingfor summary in theCRMMAINTENANCEDeploved |...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
25151
|
1055
|
19
|
2026-05-12T10:45:09.527721+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778582709527_m2.jpg...
|
Firefox
|
SQLite Web: db.sqlite — Personal
|
True
|
sqlite.screenpipe.lakylak.xyz/audio_chunks/
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
elements_fts_config
New Tab
New Tab
Screenpipe — A elements_fts_config
New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
All docs · AFFiNE
All docs · AFFiNE
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
sqlite-web 0.7.2
sqlite-web 0.7.2
db.sqlite
db.sqlite
audio_chunks
Query
Query
table name...
_sqlx_migrations
_sqlx_migrations
audio_chunks
audio_chunks
audio_tags
audio_tags
audio_transcriptions
audio_transcriptions
audio_transcriptions_fts (v)
audio_transcriptions_fts
(v)
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
elements
elements
elements_fts (v)
elements_fts
(v)
elements_fts_config
elements_fts_config
elements_fts_data
elements_fts_data
elements_fts_idx
elements_fts_idx
frames
frames
frames_fts (v)
frames_fts
(v)
frames_fts_config
frames_fts_config
frames_fts_data
frames_fts_data
frames_fts_idx
frames_fts_idx
meetings
meetings
memories
memories
memories_fts (v)
memories_fts
(v)
memories_fts_config
memories_fts_config
memories_fts_data
memories_fts_data
memories_fts_idx
memories_fts_idx
ocr_text
ocr_text
pipe_executions
pipe_executions
pipe_scheduler_state
pipe_scheduler_state
secrets
secrets
speaker_embeddings
speaker_embeddings
speakers
speakers
sqlite_sequence
sqlite_sequence
sqlite_stat1
sqlite_stat1
sqlite_stat4
sqlite_stat4
tags
tags
ui_events
ui_events
ui_events_fts (v)
ui_events_fts
(v)
ui_events_fts_config
ui_events_fts_config
ui_events_fts_data
ui_events_fts_data
ui_events_fts_idx
ui_events_fts_idx
video_chunks
video_chunks
vision_tags
vision_tags
Toggle helper tables
Toggle helper tables
Structure
Structure
Content
Content
Query
Query
Export
Export
SQL
SQL
CREATE
TABLE
audio_chunks
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT
,
file_path
TEXT
NOT
NULL
,
timestamp
TIMESTAMP
,
sync_id
TEXT
,
machine_id
TEXT
,
synced_at
DATETIME
,
evicted_at
TIMESTAMP
DEFAULT
NULL
)
Columns
Columns
Column
Data type
Allow null
Primary key
Actions
id
INTEGER
✓
✓
read-only
file_path
TEXT
read-only
timestamp
TIMESTAMP
✓
read-only
sync_id
TEXT
✓
read-only
machine_id
TEXT
✓
read-only
synced_at
DATETIME
✓
read-only
evicted_at
TIMESTAMP
✓
read-only
Column
id
file_path
timestamp
sync_id
machine_id
synced_at
evicted_at
Data type
INTEGER
TEXT
TIMESTAMP
TEXT
TEXT
DATETIME
TIMESTAMP
Allow null
✓
✓
✓
✓
✓
✓
Primary key
✓
Actions
read-only
read-only
read-only
read-only
read-only
read-only
read-only
Indexes
Indexes
Name
Columns
Unique
SQL
Drop?
idx_audio_chunks_timestamp
timestamp
SQL
SQL
read-only
Name
idx_audio_chunks_timestamp
Columns
timestamp
Unique
SQL
SQL
SQL
Drop?
read-only
SQLite database browser
v0.7.2
, powered by
Flask
Flask
and
Peewee
Peewee
. © 2026
Charles Leifer
Charles Leifer
sqlite.screenpipe.lakylak.xyz/elements_fts_config/...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"elements_fts_config","depth":2,"bounds":{"left":0.6130319,"top":0.41660017,"width":0.03557181,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.5,"top":0.0518755,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.51329786,"top":0.06304868,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.5,"top":0.08459697,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.51329786,"top":0.09577015,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.5,"top":0.11731844,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.51329786,"top":0.12849163,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.5,"top":0.15003991,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.51329786,"top":0.16121309,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.5,"top":0.18276137,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.51329786,"top":0.19393456,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.5,"top":0.21548285,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.51329786,"top":0.22665602,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.5,"top":0.2482043,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.51329786,"top":0.25937748,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.5,"top":0.28092578,"width":0.06881649,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.51329786,"top":0.29209897,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.55651593,"top":0.28810853,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.5028258,"top":0.31524342,"width":0.06333112,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.5028258,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.51379657,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.5249335,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.53607047,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.5472075,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"sqlite-web 0.7.2","depth":7,"bounds":{"left":0.57413566,"top":0.058260176,"width":0.043218084,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite-web 0.7.2","depth":8,"bounds":{"left":0.57413566,"top":0.06464485,"width":0.043218084,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"db.sqlite","depth":10,"bounds":{"left":0.62267286,"top":0.05865922,"width":0.023936171,"height":0.029928172},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"db.sqlite","depth":11,"bounds":{"left":0.6253325,"top":0.066640064,"width":0.01861702,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"audio_chunks","depth":10,"bounds":{"left":0.64660907,"top":0.066640064,"width":0.030086435,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Query","depth":8,"bounds":{"left":0.97639626,"top":0.061851557,"width":0.018284574,"height":0.023543496},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Query","depth":9,"bounds":{"left":0.9793883,"top":0.06743815,"width":0.012300532,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextField","text":"table name...","depth":7,"bounds":{"left":0.5738032,"top":0.111332804,"width":0.061835106,"height":0.023942538},"on_screen":true,"help_text":"","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"_sqlx_migrations","depth":9,"bounds":{"left":0.5738032,"top":0.13527533,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_sqlx_migrations","depth":10,"bounds":{"left":0.57712764,"top":0.13926576,"width":0.038896278,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_chunks","depth":9,"bounds":{"left":0.5738032,"top":0.15802075,"width":0.06200133,"height":0.02434158},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_chunks","depth":10,"bounds":{"left":0.5774601,"top":0.16280925,"width":0.031416222,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_tags","depth":9,"bounds":{"left":0.5738032,"top":0.18236233,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_tags","depth":10,"bounds":{"left":0.57712764,"top":0.18635276,"width":0.025099734,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions","depth":9,"bounds":{"left":0.5738032,"top":0.20510775,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions","depth":10,"bounds":{"left":0.57712764,"top":0.20909816,"width":0.04654255,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.22785315,"width":0.06200133,"height":0.040702313},"on_screen":true,"help_text":"audio_transcriptions_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_fts","depth":10,"bounds":{"left":0.57712764,"top":0.23184358,"width":0.055352394,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.57712764,"top":0.2482043,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_...","depth":9,"bounds":{"left":0.5738032,"top":0.26855546,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"audio_transcriptions_fts_config","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_...","depth":10,"bounds":{"left":0.57712764,"top":0.2725459,"width":0.053523935,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_...","depth":9,"bounds":{"left":0.5738032,"top":0.29130086,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"audio_transcriptions_fts_data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_...","depth":10,"bounds":{"left":0.57712764,"top":0.2952913,"width":0.053523935,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"audio_transcriptions_...","depth":9,"bounds":{"left":0.5738032,"top":0.3140463,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"audio_transcriptions_fts_idx","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"audio_transcriptions_...","depth":10,"bounds":{"left":0.57712764,"top":0.3180367,"width":0.053523935,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements","depth":9,"bounds":{"left":0.5738032,"top":0.3367917,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements","depth":10,"bounds":{"left":0.57712764,"top":0.34078214,"width":0.020777926,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.35953712,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"elements_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts","depth":10,"bounds":{"left":0.57712764,"top":0.36352754,"width":0.030917553,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6080452,"top":0.36193135,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.38228253,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.38627294,"width":0.04637633,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.40502793,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts_data","depth":10,"bounds":{"left":0.57712764,"top":0.40901837,"width":0.042220745,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"elements_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":0.42777336,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"elements_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":0.43176377,"width":0.0390625,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames","depth":9,"bounds":{"left":0.5738032,"top":0.45051876,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames","depth":10,"bounds":{"left":0.57712764,"top":0.45450917,"width":0.015791224,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.47326416,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"frames_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts","depth":10,"bounds":{"left":0.57712764,"top":0.4772546,"width":0.025930852,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6030585,"top":0.47565842,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.4960096,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.5,"width":0.04138963,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.51875496,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts_data","depth":10,"bounds":{"left":0.57712764,"top":0.52274543,"width":0.03723404,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frames_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":0.5415004,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frames_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":0.5454908,"width":0.033909574,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"meetings","depth":9,"bounds":{"left":0.5738032,"top":0.5642458,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"meetings","depth":10,"bounds":{"left":0.57712764,"top":0.56823623,"width":0.020944148,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories","depth":9,"bounds":{"left":0.5738032,"top":0.58699125,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories","depth":10,"bounds":{"left":0.57712764,"top":0.59098166,"width":0.02244016,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.6097366,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"memories_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts","depth":10,"bounds":{"left":0.57712764,"top":0.61372703,"width":0.032579787,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6097075,"top":0.6121309,"width":0.004986702,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.63248205,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.63647246,"width":0.048038565,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.6552275,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts_data","depth":10,"bounds":{"left":0.57712764,"top":0.6592179,"width":0.043882977,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"memories_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":0.67797285,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"memories_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":0.68196326,"width":0.04055851,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ocr_text","depth":9,"bounds":{"left":0.5738032,"top":0.7007183,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ocr_text","depth":10,"bounds":{"left":0.57712764,"top":0.7047087,"width":0.018783245,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"pipe_executions","depth":9,"bounds":{"left":0.5738032,"top":0.7234637,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"pipe_executions","depth":10,"bounds":{"left":0.57712764,"top":0.7274541,"width":0.036901597,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"pipe_scheduler_state","depth":9,"bounds":{"left":0.5738032,"top":0.7462091,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"pipe_scheduler_state","depth":10,"bounds":{"left":0.57712764,"top":0.7501995,"width":0.049035903,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secrets","depth":9,"bounds":{"left":0.5738032,"top":0.7689545,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secrets","depth":10,"bounds":{"left":0.57712764,"top":0.7729449,"width":0.016788565,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"speaker_embeddings","depth":9,"bounds":{"left":0.5738032,"top":0.79169995,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"speaker_embeddings","depth":10,"bounds":{"left":0.57712764,"top":0.79569036,"width":0.04886968,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"speakers","depth":9,"bounds":{"left":0.5738032,"top":0.8144453,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"speakers","depth":10,"bounds":{"left":0.57712764,"top":0.8184357,"width":0.020611702,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"sqlite_sequence","depth":9,"bounds":{"left":0.5738032,"top":0.83719075,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite_sequence","depth":10,"bounds":{"left":0.57712764,"top":0.84118116,"width":0.03723404,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"sqlite_stat1","depth":9,"bounds":{"left":0.5738032,"top":0.8599362,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite_stat1","depth":10,"bounds":{"left":0.57712764,"top":0.8639266,"width":0.025930852,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"sqlite_stat4","depth":9,"bounds":{"left":0.5738032,"top":0.88268155,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sqlite_stat4","depth":10,"bounds":{"left":0.57712764,"top":0.88667196,"width":0.026595745,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"tags","depth":9,"bounds":{"left":0.5738032,"top":0.905427,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"tags","depth":10,"bounds":{"left":0.57712764,"top":0.9094174,"width":0.009973404,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events","depth":9,"bounds":{"left":0.5738032,"top":0.9281724,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events","depth":10,"bounds":{"left":0.57712764,"top":0.9321628,"width":0.021775266,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts (v)","depth":9,"bounds":{"left":0.5738032,"top":0.9509178,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"ui_events_fts","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts","depth":10,"bounds":{"left":0.57712764,"top":0.9549082,"width":0.031914894,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(v)","depth":11,"bounds":{"left":0.6090425,"top":0.95331204,"width":0.0048204786,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts_config","depth":9,"bounds":{"left":0.5738032,"top":0.9736632,"width":0.06200133,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts_config","depth":10,"bounds":{"left":0.57712764,"top":0.9776536,"width":0.04737367,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts_data","depth":9,"bounds":{"left":0.5738032,"top":0.99640864,"width":0.06200133,"height":0.0035913587},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts_data","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.043218084,"height":-0.0003989935},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ui_events_fts_idx","depth":9,"bounds":{"left":0.5738032,"top":1.0,"width":0.06200133,"height":-0.019154072},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ui_events_fts_idx","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.039893616,"height":-0.023144484},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"video_chunks","depth":9,"bounds":{"left":0.5738032,"top":1.0,"width":0.06200133,"height":-0.041899443},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"video_chunks","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.03125,"height":-0.045889854},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"vision_tags","depth":9,"bounds":{"left":0.5738032,"top":1.0,"width":0.06200133,"height":-0.06464481},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"vision_tags","depth":10,"bounds":{"left":0.57712764,"top":1.0,"width":0.025930852,"height":-0.068635225},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Toggle helper tables","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Toggle helper tables","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Structure","depth":9,"bounds":{"left":0.6456117,"top":0.111332804,"width":0.03274601,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Structure","depth":10,"bounds":{"left":0.6512633,"top":0.12011173,"width":0.02144282,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Content","depth":9,"bounds":{"left":0.6783577,"top":0.111332804,"width":0.029421542,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Content","depth":10,"bounds":{"left":0.6840093,"top":0.12011173,"width":0.018118352,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Query","depth":9,"bounds":{"left":0.7077792,"top":0.111332804,"width":0.025265958,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Query","depth":10,"bounds":{"left":0.7134308,"top":0.12011173,"width":0.013962766,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Export","depth":9,"bounds":{"left":0.7330452,"top":0.111332804,"width":0.026097074,"height":0.032322425},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Export","depth":10,"bounds":{"left":0.7386968,"top":0.12011173,"width":0.014793883,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"SQL","depth":7,"bounds":{"left":0.6456117,"top":0.15961692,"width":0.3494016,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQL","depth":8,"bounds":{"left":0.6456117,"top":0.15961692,"width":0.016289894,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CREATE","depth":8,"bounds":{"left":0.6456117,"top":0.19273743,"width":0.015791224,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TABLE","depth":8,"bounds":{"left":0.6640625,"top":0.19273743,"width":0.013131649,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"audio_chunks","depth":8,"bounds":{"left":0.67985374,"top":0.19273743,"width":0.031416222,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":8,"bounds":{"left":0.7165891,"top":0.19273743,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":8,"bounds":{"left":0.6509308,"top":0.20830008,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":8,"bounds":{"left":0.6587433,"top":0.20830008,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":8,"bounds":{"left":0.67985374,"top":0.20830008,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"KEY","depth":8,"bounds":{"left":0.70079786,"top":0.20830008,"width":0.007978723,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AUTOINCREMENT","depth":8,"bounds":{"left":0.71127,"top":0.20830008,"width":0.034242023,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.74551195,"top":0.20830008,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"file_path","depth":8,"bounds":{"left":0.6509308,"top":0.22386272,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.6771942,"top":0.22386272,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOT","depth":8,"bounds":{"left":0.6903258,"top":0.22386272,"width":0.0078125,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.70079786,"top":0.22386272,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.71127,"top":0.22386272,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":8,"bounds":{"left":0.6509308,"top":0.23982441,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":8,"bounds":{"left":0.6771942,"top":0.23982441,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.70079786,"top":0.23982441,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sync_id","depth":8,"bounds":{"left":0.6509308,"top":0.25538707,"width":0.018450798,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.671875,"top":0.25538707,"width":0.010638298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.6825133,"top":0.25538707,"width":0.002493351,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"machine_id","depth":8,"bounds":{"left":0.6509308,"top":0.27134877,"width":0.026263298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":8,"bounds":{"left":0.67985374,"top":0.27134877,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.6903258,"top":0.27134877,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"synced_at","depth":8,"bounds":{"left":0.6509308,"top":0.2869114,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DATETIME","depth":8,"bounds":{"left":0.6771942,"top":0.2869114,"width":0.020944148,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":8,"bounds":{"left":0.6981383,"top":0.2869114,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"evicted_at","depth":8,"bounds":{"left":0.6509308,"top":0.30247405,"width":0.026263298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":8,"bounds":{"left":0.67985374,"top":0.30247405,"width":0.023603724,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEFAULT","depth":8,"bounds":{"left":0.70611703,"top":0.30247405,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":8,"bounds":{"left":0.72706115,"top":0.30247405,"width":0.010472074,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":8,"bounds":{"left":0.6456117,"top":0.31843576,"width":0.0026595744,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Columns","depth":7,"bounds":{"left":0.6456117,"top":0.3451716,"width":0.3494016,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Columns","depth":8,"bounds":{"left":0.6456117,"top":0.3451716,"width":0.032912236,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":10,"bounds":{"left":0.64960104,"top":0.3886672,"width":0.016788565,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Data type","depth":10,"bounds":{"left":0.7209109,"top":0.3886672,"width":0.021110373,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Allow null","depth":10,"bounds":{"left":0.7875665,"top":0.3886672,"width":0.021110373,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Primary key","depth":10,"bounds":{"left":0.8543883,"top":0.3886672,"width":0.025764627,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Actions","depth":10,"bounds":{"left":0.93201464,"top":0.3886672,"width":0.01662234,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":11,"bounds":{"left":0.64960104,"top":0.42697525,"width":0.004654255,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":11,"bounds":{"left":0.7209109,"top":0.42697525,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.4253791,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8543883,"top":0.4253791,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.4253791,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"file_path","depth":11,"bounds":{"left":0.64960104,"top":0.46249002,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.7209109,"top":0.46249002,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.46089387,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":11,"bounds":{"left":0.64960104,"top":0.49840382,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":11,"bounds":{"left":0.7209109,"top":0.49840382,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.49680766,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.49680766,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sync_id","depth":11,"bounds":{"left":0.64960104,"top":0.5343176,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.7209109,"top":0.5343176,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.53272146,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.53272146,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"machine_id","depth":11,"bounds":{"left":0.64960104,"top":0.5698324,"width":0.023105053,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.7209109,"top":0.5698324,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.56823623,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.56823623,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"synced_at","depth":11,"bounds":{"left":0.64960104,"top":0.6057462,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DATETIME","depth":11,"bounds":{"left":0.7209109,"top":0.6057462,"width":0.018450798,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.60415006,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.60415006,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"evicted_at","depth":11,"bounds":{"left":0.64960104,"top":0.641261,"width":0.023105053,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":11,"bounds":{"left":0.7209109,"top":0.641261,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.6396648,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.6396648,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":10,"bounds":{"left":0.64960104,"top":0.3886672,"width":0.016788565,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":11,"bounds":{"left":0.64960104,"top":0.42697525,"width":0.004654255,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"file_path","depth":11,"bounds":{"left":0.64960104,"top":0.46249002,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":11,"bounds":{"left":0.64960104,"top":0.49840382,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sync_id","depth":11,"bounds":{"left":0.64960104,"top":0.5343176,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"machine_id","depth":11,"bounds":{"left":0.64960104,"top":0.5698324,"width":0.023105053,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"synced_at","depth":11,"bounds":{"left":0.64960104,"top":0.6057462,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"evicted_at","depth":11,"bounds":{"left":0.64960104,"top":0.641261,"width":0.023105053,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Data type","depth":10,"bounds":{"left":0.7209109,"top":0.3886672,"width":0.021110373,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INTEGER","depth":11,"bounds":{"left":0.7209109,"top":0.42697525,"width":0.016289894,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.7209109,"top":0.46249002,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":11,"bounds":{"left":0.7209109,"top":0.49840382,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.7209109,"top":0.5343176,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TEXT","depth":11,"bounds":{"left":0.7209109,"top":0.5698324,"width":0.00930851,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DATETIME","depth":11,"bounds":{"left":0.7209109,"top":0.6057462,"width":0.018450798,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TIMESTAMP","depth":11,"bounds":{"left":0.7209109,"top":0.641261,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Allow null","depth":10,"bounds":{"left":0.7875665,"top":0.3886672,"width":0.021110373,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.4253791,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.49680766,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.53272146,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.56823623,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.60415006,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.7875665,"top":0.6396648,"width":0.0038231383,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Primary key","depth":10,"bounds":{"left":0.8543883,"top":0.3886672,"width":0.025764627,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"✓","depth":10,"bounds":{"left":0.8543883,"top":0.4253791,"width":0.003656915,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Actions","depth":10,"bounds":{"left":0.93201464,"top":0.3886672,"width":0.01662234,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.4253791,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.46089387,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.49680766,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.53272146,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.56823623,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.60415006,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.93201464,"top":0.6396648,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Indexes","depth":7,"bounds":{"left":0.6456117,"top":0.68994415,"width":0.3494016,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Indexes","depth":8,"bounds":{"left":0.6456117,"top":0.68994415,"width":0.028590426,"height":0.025139665},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Name","depth":10,"bounds":{"left":0.64960104,"top":0.73343974,"width":0.012632979,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Columns","depth":10,"bounds":{"left":0.7932181,"top":0.73343974,"width":0.019115692,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Unique","depth":10,"bounds":{"left":0.85355717,"top":0.73343974,"width":0.015292553,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQL","depth":10,"bounds":{"left":0.90242684,"top":0.73343974,"width":0.008976064,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Drop?","depth":10,"bounds":{"left":0.9378325,"top":0.73343974,"width":0.013131649,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idx_audio_chunks_timestamp","depth":10,"bounds":{"left":0.64960104,"top":0.7701516,"width":0.06067154,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":11,"bounds":{"left":0.7932181,"top":0.7717478,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SQL","depth":10,"bounds":{"left":0.90242684,"top":0.7701516,"width":0.008643617,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQL","depth":11,"bounds":{"left":0.90242684,"top":0.7701516,"width":0.008643617,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9378325,"top":0.7701516,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Name","depth":10,"bounds":{"left":0.64960104,"top":0.73343974,"width":0.012632979,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idx_audio_chunks_timestamp","depth":10,"bounds":{"left":0.64960104,"top":0.7701516,"width":0.06067154,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Columns","depth":10,"bounds":{"left":0.7932181,"top":0.73343974,"width":0.019115692,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp","depth":11,"bounds":{"left":0.7932181,"top":0.7717478,"width":0.020777926,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Unique","depth":10,"bounds":{"left":0.85355717,"top":0.73343974,"width":0.015292553,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQL","depth":10,"bounds":{"left":0.90242684,"top":0.73343974,"width":0.008976064,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SQL","depth":10,"bounds":{"left":0.90242684,"top":0.7701516,"width":0.008643617,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQL","depth":11,"bounds":{"left":0.90242684,"top":0.7701516,"width":0.008643617,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Drop?","depth":10,"bounds":{"left":0.9378325,"top":0.73343974,"width":0.013131649,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"read-only","depth":11,"bounds":{"left":0.9378325,"top":0.7701516,"width":0.021276595,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQLite database browser","depth":9,"bounds":{"left":0.6456117,"top":0.8367917,"width":0.05236037,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"v0.7.2","depth":9,"bounds":{"left":0.69797206,"top":0.8367917,"width":0.011801862,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", powered by","depth":9,"bounds":{"left":0.70977396,"top":0.8367917,"width":0.027260639,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Flask","depth":9,"bounds":{"left":0.73703456,"top":0.8367917,"width":0.010638298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Flask","depth":10,"bounds":{"left":0.73703456,"top":0.8367917,"width":0.010638298,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":9,"bounds":{"left":0.74767286,"top":0.8367917,"width":0.009973404,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Peewee","depth":9,"bounds":{"left":0.75764626,"top":0.8367917,"width":0.015791224,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Peewee","depth":10,"bounds":{"left":0.75764626,"top":0.8367917,"width":0.015791224,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". © 2026","depth":9,"bounds":{"left":0.7734375,"top":0.8367917,"width":0.019614361,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Charles Leifer","depth":9,"bounds":{"left":0.79305184,"top":0.8367917,"width":0.028091755,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Charles Leifer","depth":10,"bounds":{"left":0.79305184,"top":0.8367917,"width":0.028091755,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sqlite.screenpipe.lakylak.xyz/elements_fts_config/","depth":5,"bounds":{"left":0.57014626,"top":0.9876297,"width":0.087932184,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-7241606400985790368
|
3694566442484812483
|
visual_change
|
accessibility
|
NULL
|
elements_fts_config
New Tab
New Tab
Screenpipe — A elements_fts_config
New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
All docs · AFFiNE
All docs · AFFiNE
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
sqlite-web 0.7.2
sqlite-web 0.7.2
db.sqlite
db.sqlite
audio_chunks
Query
Query
table name...
_sqlx_migrations
_sqlx_migrations
audio_chunks
audio_chunks
audio_tags
audio_tags
audio_transcriptions
audio_transcriptions
audio_transcriptions_fts (v)
audio_transcriptions_fts
(v)
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
audio_transcriptions_...
elements
elements
elements_fts (v)
elements_fts
(v)
elements_fts_config
elements_fts_config
elements_fts_data
elements_fts_data
elements_fts_idx
elements_fts_idx
frames
frames
frames_fts (v)
frames_fts
(v)
frames_fts_config
frames_fts_config
frames_fts_data
frames_fts_data
frames_fts_idx
frames_fts_idx
meetings
meetings
memories
memories
memories_fts (v)
memories_fts
(v)
memories_fts_config
memories_fts_config
memories_fts_data
memories_fts_data
memories_fts_idx
memories_fts_idx
ocr_text
ocr_text
pipe_executions
pipe_executions
pipe_scheduler_state
pipe_scheduler_state
secrets
secrets
speaker_embeddings
speaker_embeddings
speakers
speakers
sqlite_sequence
sqlite_sequence
sqlite_stat1
sqlite_stat1
sqlite_stat4
sqlite_stat4
tags
tags
ui_events
ui_events
ui_events_fts (v)
ui_events_fts
(v)
ui_events_fts_config
ui_events_fts_config
ui_events_fts_data
ui_events_fts_data
ui_events_fts_idx
ui_events_fts_idx
video_chunks
video_chunks
vision_tags
vision_tags
Toggle helper tables
Toggle helper tables
Structure
Structure
Content
Content
Query
Query
Export
Export
SQL
SQL
CREATE
TABLE
audio_chunks
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT
,
file_path
TEXT
NOT
NULL
,
timestamp
TIMESTAMP
,
sync_id
TEXT
,
machine_id
TEXT
,
synced_at
DATETIME
,
evicted_at
TIMESTAMP
DEFAULT
NULL
)
Columns
Columns
Column
Data type
Allow null
Primary key
Actions
id
INTEGER
✓
✓
read-only
file_path
TEXT
read-only
timestamp
TIMESTAMP
✓
read-only
sync_id
TEXT
✓
read-only
machine_id
TEXT
✓
read-only
synced_at
DATETIME
✓
read-only
evicted_at
TIMESTAMP
✓
read-only
Column
id
file_path
timestamp
sync_id
machine_id
synced_at
evicted_at
Data type
INTEGER
TEXT
TIMESTAMP
TEXT
TEXT
DATETIME
TIMESTAMP
Allow null
✓
✓
✓
✓
✓
✓
Primary key
✓
Actions
read-only
read-only
read-only
read-only
read-only
read-only
read-only
Indexes
Indexes
Name
Columns
Unique
SQL
Drop?
idx_audio_chunks_timestamp
timestamp
SQL
SQL
read-only
Name
idx_audio_chunks_timestamp
Columns
timestamp
Unique
SQL
SQL
SQL
Drop?
read-only
SQLite database browser
v0.7.2
, powered by
Flask
Flask
and
Peewee
Peewee
. © 2026
Charles Leifer
Charles Leifer
sqlite.screenpipe.lakylak.xyz/elements_fts_config/...
|
25150
|
NULL
|
NULL
|
NULL
|
|
21961
|
961
|
17
|
2026-05-12T06:41:03.813027+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778568063813_m2.jpg...
|
Firefox
|
JY-20725 add HS rate limit handling on activities JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app — Work...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.29986703,"top":0.0518755,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.3131649,"top":0.06304868,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"bounds":{"left":0.29986703,"top":0.08459697,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"bounds":{"left":0.3131649,"top":0.09577015,"width":0.16888298,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"bounds":{"left":0.29986703,"top":0.11731844,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"bounds":{"left":0.3131649,"top":0.12849163,"width":0.16140293,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"bounds":{"left":0.29986703,"top":0.15003991,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"bounds":{"left":0.3131649,"top":0.16121309,"width":0.4644282,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.29986703,"top":0.18276137,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.3131649,"top":0.19393456,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.3671875,"top":0.18994413,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.29986703,"top":0.21548285,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.3131649,"top":0.22665602,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.29986703,"top":0.2482043,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.3131649,"top":0.25937748,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.29986703,"top":0.28092578,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.3131649,"top":0.29209897,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.29986703,"top":0.31364724,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.3131649,"top":0.32482043,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.29986703,"top":0.3463687,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.3131649,"top":0.3575419,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.29986703,"top":0.3790902,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.3131649,"top":0.39026338,"width":0.40475398,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.29986703,"top":0.41181165,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.3131649,"top":0.42298484,"width":0.40475398,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.30269283,"top":0.4461293,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.30269283,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.31366357,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.32480052,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.3359375,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.34707448,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"bounds":{"left":0.37948802,"top":0.0518755,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"bounds":{"left":0.37948802,"top":0.05347167,"width":0.0029920214,"height":0.21468475},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"bounds":{"left":0.38480717,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"bounds":{"left":0.39943483,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"bounds":{"left":0.41273272,"top":0.06464485,"width":0.018949468,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"bounds":{"left":0.4147274,"top":0.07063048,"width":0.014960106,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"bounds":{"left":0.43666887,"top":0.06464485,"width":0.017785905,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"bounds":{"left":0.43866357,"top":0.07063048,"width":0.008477394,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"bounds":{"left":0.8166556,"top":0.06464485,"width":0.06565824,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.8289561,"top":0.07063048,"width":0.011801862,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"bounds":{"left":0.84208775,"top":0.07222666,"width":0.002493351,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"bounds":{"left":0.84607714,"top":0.07063048,"width":0.021276595,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"bounds":{"left":0.8843085,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"bounds":{"left":0.89461434,"top":0.06464485,"width":0.008643617,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"bounds":{"left":0.9112367,"top":0.06464485,"width":0.01662234,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"All issues(g then i)","depth":9,"bounds":{"left":0.9305186,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All pull requests","depth":9,"bounds":{"left":0.9438165,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All repositories","depth":9,"bounds":{"left":0.95711434,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"bounds":{"left":0.97041225,"top":0.06464485,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7240976094876106848
|
-2723249290952312178
|
click
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)...
|
21959
|
NULL
|
NULL
|
NULL
|
|
21046
|
919
|
24
|
2026-05-11T17:05:06.185765+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778519106185_m2.jpg...
|
Code
|
Visual Studio Code
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
Opening Remote...
Opening Remote...
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: (details) Initializing VS Code Server
Setting up SSH Host nas: (
details
details
) Initializing VS Code Server...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"bounds":{"left":0.0,"top":0.047885075,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.057462092,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"bounds":{"left":0.0,"top":0.08619314,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.09577015,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G)","depth":19,"bounds":{"left":0.0,"top":0.1245012,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.13407822,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"bounds":{"left":0.0,"top":0.16280925,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.17238627,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"bounds":{"left":0.0,"top":0.20111732,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.21069433,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X)","depth":19,"bounds":{"left":0.0,"top":0.23942538,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.2490024,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open Chat","depth":28,"bounds":{"left":0.46276596,"top":0.5786113,"width":0.023271276,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.5786113,"width":0.003656915,"height":0.012769354}},{"char_start":1,"char_count":8,"bounds":{"left":0.4664229,"top":0.5786113,"width":0.019614361,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"bounds":{"left":0.52892286,"top":0.5794094,"width":0.0033244682,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.5794094,"width":0.003656915,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"I","depth":29,"bounds":{"left":0.54853725,"top":0.5794094,"width":0.0013297872,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Show All Commands","depth":28,"bounds":{"left":0.46276596,"top":0.59936154,"width":0.044215426,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.60015965,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.46575797,"top":0.60015965,"width":0.041223403,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⇧","depth":29,"bounds":{"left":0.52892286,"top":0.60015965,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.60015965,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"P","depth":29,"bounds":{"left":0.54787236,"top":0.60015965,"width":0.0026595744,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open Recent","depth":28,"bounds":{"left":0.46276596,"top":0.6201117,"width":0.02825798,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.6209098,"width":0.003656915,"height":0.011971269}},{"char_start":1,"char_count":10,"bounds":{"left":0.4664229,"top":0.6209098,"width":0.024601065,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"bounds":{"left":0.5382314,"top":0.6209098,"width":0.0033244682,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"R","depth":29,"bounds":{"left":0.54787236,"top":0.6209098,"width":0.0026595744,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open File or Folder","depth":28,"bounds":{"left":0.46276596,"top":0.64166003,"width":0.041888297,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.64166003,"width":0.003656915,"height":0.012769354}},{"char_start":1,"char_count":18,"bounds":{"left":0.4664229,"top":0.64166003,"width":0.037898935,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.6424581,"width":0.003656915,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"O","depth":29,"bounds":{"left":0.54787236,"top":0.6424581,"width":0.0029920214,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"New Untitled Text File","depth":28,"bounds":{"left":0.46276596,"top":0.6624102,"width":0.047872342,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.6624102,"width":0.0033244682,"height":0.012769354}},{"char_start":1,"char_count":21,"bounds":{"left":0.4660904,"top":0.6624102,"width":0.04454787,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.6632083,"width":0.003656915,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"N","depth":29,"bounds":{"left":0.54787236,"top":0.6632083,"width":0.0029920214,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Opening Remote...","depth":16,"bounds":{"left":0.0006648936,"top":0.98244214,"width":0.04654255,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.0019946808,"top":0.98244214,"width":0.0076462766,"height":0.01755786},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Opening Remote...","depth":17,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.035904255,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":16,"bounds":{"left":0.009973404,"top":0.9856345,"width":0.03357713,"height":0.011173184}}],"role_description":"text"},{"role":"AXButton","text":"No Problems","depth":16,"bounds":{"left":0.048204787,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.049867023,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.05518617,"top":0.9856345,"width":0.004986702,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.059840426,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.065159574,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"No Ports Forwarded","depth":16,"bounds":{"left":0.07247341,"top":0.98244214,"width":0.012632979,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.07413564,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.07945479,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"bounds":{"left":0.9886968,"top":0.98244214,"width":0.010638298,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"bounds":{"left":0.9650931,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.96675533,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.013962766,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":6,"bounds":{"left":0.9734042,"top":0.9856345,"width":0.010638298,"height":0.011173184}}],"role_description":"text"},{"role":"AXStaticText","text":"Info: Setting up SSH Host nas: (details) Initializing VS Code Server","depth":12,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":16,"bounds":{"left":0.8507314,"top":0.95211494,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Setting up SSH Host nas: (","depth":16,"bounds":{"left":0.8580452,"top":0.952913,"width":0.053523935,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.8580452,"top":0.9537111,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":25,"bounds":{"left":0.8607048,"top":0.9537111,"width":0.05086436,"height":0.011971269}}],"role_description":"text"},{"role":"AXLink","text":"details","depth":16,"bounds":{"left":0.9115692,"top":0.952913,"width":0.013630319,"height":0.011971269},"on_screen":true,"help_text":"Show details","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"details","depth":17,"bounds":{"left":0.9115692,"top":0.952913,"width":0.013630319,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.9115692,"top":0.9537111,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":6,"bounds":{"left":0.91422874,"top":0.9537111,"width":0.010638298,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":") Initializing VS Code Server","depth":16,"bounds":{"left":0.92486703,"top":0.952913,"width":0.05618351,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.92486703,"top":0.9537111,"width":0.0019946808,"height":0.011971269}},{"char_start":1,"char_count":28,"bounds":{"left":0.9265292,"top":0.9537111,"width":0.054521278,"height":0.011971269}}],"role_description":"text"}]...
|
-7240395499493211423
|
-1771713794058649249
|
visual_change
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
Opening Remote...
Opening Remote...
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: (details) Initializing VS Code Server
Setting up SSH Host nas: (
details
details
) Initializing VS Code Server...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22274
|
968
|
2
|
2026-05-12T06:58:57.694309+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778569137694_m1.jpg...
|
Firefox
|
LLM API Pricing Comparison & Cost Guide (May 2 LLM API Pricing Comparison & Cost Guide (May 2026) — Work...
|
True
|
costgoat.com/compare/llm-api
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
LLM API Pricing Comparison & Cost Guide (May 2026)
LLM API Pricing Comparison & Cost Guide (May 2026)
Close tab
New Tab
Customize sidebar
Close Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
AI Chat settings...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"LLM API Pricing Comparison & Cost Guide (May 2026)","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"LLM API Pricing Comparison & Cost Guide (May 2026)","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.019097222,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04236111,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Chat settings","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7240381804647448462
|
-2731131685252269426
|
visual_change
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
LLM API Pricing Comparison & Cost Guide (May 2026)
LLM API Pricing Comparison & Cost Guide (May 2026)
Close tab
New Tab
Customize sidebar
Close Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
AI Chat settings...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
25179
|
1054
|
26
|
2026-05-12T10:47:10.363372+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778582830363_m1.jpg...
|
Firefox
|
JY-20773 fix user pilot tracking for automated rep JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app — Work...
|
True
|
github.com/jiminny/app/pull/12024
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (1)
Security and quality
(
1
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
JY-20773 fix user pilot tracking for automated report generated #12024 Edit title
JY-20773 fix user pilot tracking for automated report generated
#
12024
Edit title
Checks pending
Checks pending
Code
Code
Open
LakyLak
LakyLak
wants to merge 3 commits into
master
master
from
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Copy head branch name to clipboard
Lines changed: 3 additions & 0 deletions
Conversation (1)
Conversation
(
1
)
Commits (3)
Commits
(
3
)
Checks (4)
Checks
(
4
)
Files changed (1)
Files changed
(
1
)
Open
JY-20773 fix user pilot tracking for automated report generated #12024 LakyLak wants to merge 3 commits into master from JY-20773-fix-automated-reports-user-pilot-tracking Copy head branch name to clipboard
JY-20773 fix user pilot tracking for automated report generated
JY-20773 fix user pilot tracking for automated report generated
#
12024
LakyLak
LakyLak
wants to merge 3 commits into
master
master
from
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Copy head branch name to clipboard
Conversation
Conversation
@LakyLak
Show options
LakyLak commented 2 weeks ago
LakyLak
LakyLak
commented
2 weeks ago
2 weeks ago
JIRA: JY-20773
JIRA:
JY-20773
JY-20773
Changes:
Changes:
Add SerializesModels trait
Add or remove reactions
LakyLak
LakyLak
and others
added
2
commits
2 weeks ago
2 weeks ago
@LakyLak
JY-20773
JY-20773
fix user pilot tracking ofr automated report generated
fix user pilot tracking ofr automated report generated
8 / 10 checks OK
6bd41c8
6bd41c8
@LakyLak
Merge branch 'master' into
Merge branch 'master' into
JY-20773
JY-20773
-fix-automated-reports-user-pilot-…
-fix-automated-reports-user-pilot-…
…
Verified
Verified
12 / 13 checks OK
5b2c922
5b2c922
@sonarqubecloud
Show options
sonarqubecloud Bot commented 24 minutes ago
sonarqubecloud
sonarqubecloud
Bot
commented
24 minutes ago
24 minutes ago
Quality Gate Passed Quality Gate passed
Quality Gate Passed
Quality Gate passed
Issues
0 New issues
0 New issues
0 Accepted issues
0 Accepted issues
Measures
0 Security Hotspots
0 Security Hotspots
0.0% Coverage on New Code
0.0% Coverage on New Code
0.0% Duplication on New Code
0.0% Duplication on New Code
See analysis details on SonarQube Cloud
See analysis details on SonarQube Cloud
Add or remove reactions
@LakyLak
LakyLak
LakyLak
changed the title...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Unnamed Group","depth":4,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"CloudWatch | us-east-2","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CloudWatch | us-east-2","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unnamed Group","depth":4,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Data Explorer","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Data Explorer","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Team - Backlog - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team - Backlog - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"All issues(g then i)","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All pull requests","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"All repositories","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (1)","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"JY-20773 fix user pilot tracking for automated report generated #12024 Edit title","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12024","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"LakyLak","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 3 commits into","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"on_screen":false,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":16,"on_screen":false,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 3 additions & 0 deletions","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (1)","depth":16,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (3)","depth":16,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (4)","depth":16,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20773 fix user pilot tracking for automated report generated #12024 LakyLak wants to merge 3 commits into master from JY-20773-fix-automated-reports-user-pilot-tracking Copy head branch name to clipboard","depth":14,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773 fix user pilot tracking for automated report generated","depth":16,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12024","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"LakyLak","depth":18,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":19,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 3 commits into","depth":18,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"on_screen":true,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":19,"on_screen":true,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":20,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"LakyLak commented 2 weeks ago","depth":14,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"LakyLak","depth":16,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 weeks ago","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 weeks ago","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20773","depth":16,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773","depth":17,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Add SerializesModels trait","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"on_screen":false,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"LakyLak","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and others","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"added","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commits","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 weeks ago","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2 weeks ago","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"JY-20773","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"fix user pilot tracking ofr automated report generated","depth":14,"on_screen":false,"help_text":"JY-20773 fix user pilot tracking ofr automated report generated","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix user pilot tracking ofr automated report generated","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"8 / 10 checks OK","depth":14,"on_screen":false,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"6bd41c8","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6bd41c8","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@LakyLak","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Merge branch 'master' into","depth":14,"on_screen":false,"help_text":"Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Merge branch 'master' into","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"-fix-automated-reports-user-pilot-…","depth":14,"on_screen":false,"help_text":"Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"-fix-automated-reports-user-pilot-…","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"…","depth":14,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Verified","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Verified","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"12 / 13 checks OK","depth":14,"on_screen":false,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"5b2c922","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5b2c922","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@sonarqubecloud","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"sonarqubecloud Bot commented 24 minutes ago","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"sonarqubecloud","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"sonarqubecloud","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bot","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"24 minutes ago","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"24 minutes ago","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Quality Gate Passed Quality Gate passed","depth":16,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"Quality Gate Passed","depth":17,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Quality Gate passed","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Issues","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 New issues","depth":17,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0 New issues","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Accepted issues","depth":17,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0 Accepted issues","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Measures","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Security Hotspots","depth":17,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0 Security Hotspots","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0.0% Coverage on New Code","depth":17,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0.0% Coverage on New Code","depth":18,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0.0% Duplication on New Code","depth":17,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0.0% Duplication on New Code","depth":18,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"See analysis details on SonarQube Cloud","depth":17,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"See analysis details on SonarQube Cloud","depth":18,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"on_screen":true,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@LakyLak","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"LakyLak","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LakyLak","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"changed the title","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-7240375949787709051
|
-138387344712566656
|
idle
|
accessibility
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
All issues(g then i)
All pull requests
All repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (1)
Security and quality
(
1
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
JY-20773 fix user pilot tracking for automated report generated #12024 Edit title
JY-20773 fix user pilot tracking for automated report generated
#
12024
Edit title
Checks pending
Checks pending
Code
Code
Open
LakyLak
LakyLak
wants to merge 3 commits into
master
master
from
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Copy head branch name to clipboard
Lines changed: 3 additions & 0 deletions
Conversation (1)
Conversation
(
1
)
Commits (3)
Commits
(
3
)
Checks (4)
Checks
(
4
)
Files changed (1)
Files changed
(
1
)
Open
JY-20773 fix user pilot tracking for automated report generated #12024 LakyLak wants to merge 3 commits into master from JY-20773-fix-automated-reports-user-pilot-tracking Copy head branch name to clipboard
JY-20773 fix user pilot tracking for automated report generated
JY-20773 fix user pilot tracking for automated report generated
#
12024
LakyLak
LakyLak
wants to merge 3 commits into
master
master
from
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Copy head branch name to clipboard
Conversation
Conversation
@LakyLak
Show options
LakyLak commented 2 weeks ago
LakyLak
LakyLak
commented
2 weeks ago
2 weeks ago
JIRA: JY-20773
JIRA:
JY-20773
JY-20773
Changes:
Changes:
Add SerializesModels trait
Add or remove reactions
LakyLak
LakyLak
and others
added
2
commits
2 weeks ago
2 weeks ago
@LakyLak
JY-20773
JY-20773
fix user pilot tracking ofr automated report generated
fix user pilot tracking ofr automated report generated
8 / 10 checks OK
6bd41c8
6bd41c8
@LakyLak
Merge branch 'master' into
Merge branch 'master' into
JY-20773
JY-20773
-fix-automated-reports-user-pilot-…
-fix-automated-reports-user-pilot-…
…
Verified
Verified
12 / 13 checks OK
5b2c922
5b2c922
@sonarqubecloud
Show options
sonarqubecloud Bot commented 24 minutes ago
sonarqubecloud
sonarqubecloud
Bot
commented
24 minutes ago
24 minutes ago
Quality Gate Passed Quality Gate passed
Quality Gate Passed
Quality Gate passed
Issues
0 New issues
0 New issues
0 Accepted issues
0 Accepted issues
Measures
0 Security Hotspots
0 Security Hotspots
0.0% Coverage on New Code
0.0% Coverage on New Code
0.0% Duplication on New Code
0.0% Duplication on New Code
See analysis details on SonarQube Cloud
See analysis details on SonarQube Cloud
Add or remove reactions
@LakyLak
LakyLak
LakyLak
changed the title...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17558
|
772
|
74
|
2026-05-11T10:25:24.579840+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778495124579_m1.jpg...
|
Firefox
|
Screenpipe — Archive — Personal
|
True
|
app.screenpipe.lakylak.xyz
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Screenpipe — Archive
Screenpipe — New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 12323.6MB]
Screenpipe
[archive.db · 12323.6MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.043402776,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.06631944,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.08958333,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.112847224,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.13611111,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Screenpipe [archive.db · 12323.6MB]","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screenpipe","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[archive.db · 12323.6MB]","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Activity","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Search","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Audio","depth":7,"on_screen":true,"help_text":"No audio data in this database","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Work Report","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Timetable","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Summary","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7238832952524157122
|
-1999179270878554477
|
click
|
accessibility
|
NULL
|
New Tab
New Tab
Screenpipe — Archive
Screenpipe — New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 12323.6MB]
Screenpipe
[archive.db · 12323.6MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
18069
|
785
|
2
|
2026-05-11T10:48:01.549764+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496481549_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicareCodeLaravelKeractorTOOISWindo PhostormVIewINavicareCodeLaravelKeractorTOOISWindowFV faVsco.js?9 JY-20725-handle-HS-search-rate-limitProiect vC) TrackAutomatedReporiGeneratedeventong(C) TranscodeParameterRescl© Hubspot/Service.php© UserService.php© Uuid.php> D TraitsD UseCases> D User> D Utils› D Validation> OvOphp nelpers.ong© InitialFrontendState.php© Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.ohpbootstrap>C build> contia>D contrib→ database>M docsM front-end>D lang> node_modules library rootM ohostan> M nublic> D resourcesv Mroutesphp api.phpphp api_v2.phppnp console.onopnp customer_api.onppnp embedded.ongphp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage> M debuabar.… M frameworkv Mloas.aitianore• audio wav= custom.loalT SyncCrmEntitiesTrait.php© CachedCrmServiceDecorator.phpo RematchActivityonCrmObiectDetach.png© MatchActivityCrmData.php( RateLimitExceptionBadkequest.ono© CrmObiectsResolver.php() ProviderRateLimiter.phpC) PaqinationContia.phpclass Cuient extends BasecLient imolements Hubspotcuientinterface1511157167179|184 G1= hubspot-journal-poll.log187|= laravel log< nhnunit ym190 Glus tht is= oauth-nrivate kevTacts naccod. 12 (15 minutes aao)public function parseRetrvAfter(Throwable Se): intif (Spolicy === 'SECONDLY') {return 11if (Spolicy === 'DAILY LIMIT') {Sthis->lo0->warnina('[Hubspotl No retrv-after header or policy name found. usina default'. ['excention class' => aet class(Se).return 10:Lusagespublic function parsePolicy(Throwable $e): ?stringif (! method_exists($elethod: 'getResponseBody')) {recurn nullSbody = Se->qetResponseBodyO:if (is string($body)) {sbody = nson decodesbody.associative: true) ?? [l:if (! is_array($body)) {Spolicy = Sbodvf 'poLicvName'1 22 Sbodvf 'policv'] 2? Sbodvf 'context'1['policvName'] >> null!return is_string(Spolicy) ? strtoupper(Spolicy) : null;nublic function aetMinimumAniVencionO• strinanetunn solf.•MTN APT VERSTON.public function getInstance: Factoryм A3670 ×3^4 SF jiminny@localhost]4 HS_local (jiminny@localhost]# console [PKoD)A console (EU]A console [STAGING][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?"Vace".L"Inu,or May 2020 14.21.15 bMl"J,"concent-lype. applicacionson-charser=utt-o'.n"Transter-Encodinq":"Chunked".i"Connection":"keep-alive"r"CF-Ray": ["9f80deb8db60dc3a-SOF"]."Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludesubbomains: preload")."server-timing": ["hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3)","x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"Set-Cookie":["__cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTg07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"•r"*"endnoints"."urz\":\"https:|\/\V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEg2\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,"report_to":|"cf-nel\"."max age":604800}"]"Server": ["cloudflare"]}} {"correlation_1d":"95256555-ec98-4541-D92a-adta/Sboveab","trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}• suppont Dally • In 1h 13m100% L2• Mon 11 May 13:48:01HandleHubspotRateLimitTest vHubspot Rate Limit Re* Expected durina storia ug" or "ggu dependina on poliicv. Emoty between TTL cvcles.+0 ..#2. Queue 1s draining (delayed queue sweuls then drains)docker exec docker lamo 1 redis-cli h redis llen "queues:analvtics low"docker exec docker_Lamp_1 redis-cli -h redis zcard "queues:analytics_low:delayed"# 3. No log flood - count rate-limit lines per seconddocker exec docker lamp 1 sh -c 'tail -n 5000 storage/logs/laravel.log | grep -c "Rate limit caught"* Exoected: dozens per minute, not thousands ner secondi#4. No stack traces from MatchActivityCrmData::handle for rate Limitsdocker exec docker lamp 1 sh -c 'grep "Failed to match CRM data" storage/logs/laravel.log | wc -l'#5. Sampled logging - count lines per attempts valuedocker exec docker_lamp_1 sh -c 'grep "Rate limit caught" storage/logs/laravel. log | grep -oE "attemp"# Expected: high counts for 1, 2, 3, 10, 20, 30, 40 - zero or few for 4-9, 11-19, 21-29Tuning the stormFor Postman to reliably trigger 429s while your jobs run• Postman concurrency: 5-10 parallel users hitting HubSpot-touching endpoints•Job count: 50-200 is enouah to see the steady state, 100k is overkill for verificationlWorkers: 5-10. More workers = more visible thundering-herd-vs-cache dynamics.• HubSpot test portal: ideallv use a non-production portal with low traffic so vou don't burn real quota.If Dostman alone canlt reliablv triader 420c (HuhSnot'c actual nortal limits are hicher than 5 PDS for non-cearchendpoints), focus vour simulation on matchßvName / search calls — those are bound at 5 RPS per portal and will rate-limitTastestSummarvFiles chanaed: JiminnvDebuaCommand.oho oniv. Three new orivate methods + handle/ now oicks one to run.To replicate1. Start workers2. Run Postman load alonaside3. php artisan jiminny: debug to fire the storm4. Tail loas + watch RedisHealthy signature in logs:• Afew (Hubspot) Received 429 lines per cache TTL cycle (not per second)•[HandleHubspotRateLmitl Rate limit caught onlv on attempts 1, 2, 3, 10. 20..• Zero [MatchActivityCrmData] Failed to match CRM data stack traces during a pure rate-limit storm• After 30 min: [MatchActivitvCrmDatal Job permanently failed after all retries for the leftover iobs (these arethe only Sentry-visible events).nl .yourscontytinte y ersufyth- PAite iolitry therteis no such thing. ("'status": ror' message: You have reached• CodeWN Windsurf Toams 20-17 UTF.8io 4 spacesTOD...
|
NULL
|
-7238820957732699034
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicareCodeLaravelKeractorTOOISWindo PhostormVIewINavicareCodeLaravelKeractorTOOISWindowFV faVsco.js?9 JY-20725-handle-HS-search-rate-limitProiect vC) TrackAutomatedReporiGeneratedeventong(C) TranscodeParameterRescl© Hubspot/Service.php© UserService.php© Uuid.php> D TraitsD UseCases> D User> D Utils› D Validation> OvOphp nelpers.ong© InitialFrontendState.php© Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.ohpbootstrap>C build> contia>D contrib→ database>M docsM front-end>D lang> node_modules library rootM ohostan> M nublic> D resourcesv Mroutesphp api.phpphp api_v2.phppnp console.onopnp customer_api.onppnp embedded.ongphp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage> M debuabar.… M frameworkv Mloas.aitianore• audio wav= custom.loalT SyncCrmEntitiesTrait.php© CachedCrmServiceDecorator.phpo RematchActivityonCrmObiectDetach.png© MatchActivityCrmData.php( RateLimitExceptionBadkequest.ono© CrmObiectsResolver.php() ProviderRateLimiter.phpC) PaqinationContia.phpclass Cuient extends BasecLient imolements Hubspotcuientinterface1511157167179|184 G1= hubspot-journal-poll.log187|= laravel log< nhnunit ym190 Glus tht is= oauth-nrivate kevTacts naccod. 12 (15 minutes aao)public function parseRetrvAfter(Throwable Se): intif (Spolicy === 'SECONDLY') {return 11if (Spolicy === 'DAILY LIMIT') {Sthis->lo0->warnina('[Hubspotl No retrv-after header or policy name found. usina default'. ['excention class' => aet class(Se).return 10:Lusagespublic function parsePolicy(Throwable $e): ?stringif (! method_exists($elethod: 'getResponseBody')) {recurn nullSbody = Se->qetResponseBodyO:if (is string($body)) {sbody = nson decodesbody.associative: true) ?? [l:if (! is_array($body)) {Spolicy = Sbodvf 'poLicvName'1 22 Sbodvf 'policv'] 2? Sbodvf 'context'1['policvName'] >> null!return is_string(Spolicy) ? strtoupper(Spolicy) : null;nublic function aetMinimumAniVencionO• strinanetunn solf.•MTN APT VERSTON.public function getInstance: Factoryм A3670 ×3^4 SF jiminny@localhost]4 HS_local (jiminny@localhost]# console [PKoD)A console (EU]A console [STAGING][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?"Vace".L"Inu,or May 2020 14.21.15 bMl"J,"concent-lype. applicacionson-charser=utt-o'.n"Transter-Encodinq":"Chunked".i"Connection":"keep-alive"r"CF-Ray": ["9f80deb8db60dc3a-SOF"]."Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludesubbomains: preload")."server-timing": ["hcid;desc=|"019e02d0-6fd8-7812-bdba-885b7ccb3ee3)","x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"Set-Cookie":["__cf_bm=S1UrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTg07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"•r"*"endnoints"."urz\":\"https:|\/\V/a.nel.cloudflare.com\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEg2\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,"report_to":|"cf-nel\"."max age":604800}"]"Server": ["cloudflare"]}} {"correlation_1d":"95256555-ec98-4541-D92a-adta/Sboveab","trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}• suppont Dally • In 1h 13m100% L2• Mon 11 May 13:48:01HandleHubspotRateLimitTest vHubspot Rate Limit Re* Expected durina storia ug" or "ggu dependina on poliicv. Emoty between TTL cvcles.+0 ..#2. Queue 1s draining (delayed queue sweuls then drains)docker exec docker lamo 1 redis-cli h redis llen "queues:analvtics low"docker exec docker_Lamp_1 redis-cli -h redis zcard "queues:analytics_low:delayed"# 3. No log flood - count rate-limit lines per seconddocker exec docker lamp 1 sh -c 'tail -n 5000 storage/logs/laravel.log | grep -c "Rate limit caught"* Exoected: dozens per minute, not thousands ner secondi#4. No stack traces from MatchActivityCrmData::handle for rate Limitsdocker exec docker lamp 1 sh -c 'grep "Failed to match CRM data" storage/logs/laravel.log | wc -l'#5. Sampled logging - count lines per attempts valuedocker exec docker_lamp_1 sh -c 'grep "Rate limit caught" storage/logs/laravel. log | grep -oE "attemp"# Expected: high counts for 1, 2, 3, 10, 20, 30, 40 - zero or few for 4-9, 11-19, 21-29Tuning the stormFor Postman to reliably trigger 429s while your jobs run• Postman concurrency: 5-10 parallel users hitting HubSpot-touching endpoints•Job count: 50-200 is enouah to see the steady state, 100k is overkill for verificationlWorkers: 5-10. More workers = more visible thundering-herd-vs-cache dynamics.• HubSpot test portal: ideallv use a non-production portal with low traffic so vou don't burn real quota.If Dostman alone canlt reliablv triader 420c (HuhSnot'c actual nortal limits are hicher than 5 PDS for non-cearchendpoints), focus vour simulation on matchßvName / search calls — those are bound at 5 RPS per portal and will rate-limitTastestSummarvFiles chanaed: JiminnvDebuaCommand.oho oniv. Three new orivate methods + handle/ now oicks one to run.To replicate1. Start workers2. Run Postman load alonaside3. php artisan jiminny: debug to fire the storm4. Tail loas + watch RedisHealthy signature in logs:• Afew (Hubspot) Received 429 lines per cache TTL cycle (not per second)•[HandleHubspotRateLmitl Rate limit caught onlv on attempts 1, 2, 3, 10. 20..• Zero [MatchActivityCrmData] Failed to match CRM data stack traces during a pure rate-limit storm• After 30 min: [MatchActivitvCrmDatal Job permanently failed after all retries for the leftover iobs (these arethe only Sentry-visible events).nl .yourscontytinte y ersufyth- PAite iolitry therteis no such thing. ("'status": ror' message: You have reached• CodeWN Windsurf Toams 20-17 UTF.8io 4 spacesTOD...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
24917
|
1045
|
2
|
2026-05-12T10:14:00.314124+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778580840314_m2.jpg...
|
Firefox
|
Pipelines - jiminny/app — Work
|
True
|
app.circleci.com/pipelines/github/jiminny/app
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Go to home page
Auto theme
Open notifications
Open support menu
Open user menu
org avatar Current organization: jiminny
Home
Home
Pipelines
Pipelines
Projects
Projects
Deploys
Deploys
Insights
Insights
Runners
Runners
Org
Org
Plan
Plan
Chunk sidecars
Chunk sidecars
PREVIEW
Chunk
Chunk
Smarter Testing is now in beta
Smarter Testing is now in beta
Get faster feedback on your tests while maintaining full confidence
Dismiss
Get Started
Get Started
Dashboard All Pipelines
All Pipelines
Project Outline app
app
app
app
Overview
Overview
Settings
Settings
Deploys
Deploys
Lightning Manage triggers
Manage triggers
Trigger Pipeline
Pipelines All pipelines my-pipelines-filter
All pipelines
app Project Filter. Selected "app"
app
All branches Branch Filter. Selected "All branches"
All branches
Start Time Cutoff date Arrow Drop Down
Cutoff date
All statuses Arrow Drop Down
All
statuses
Filter Display options
Display options
Pipeline
Status
Workflow
Checkout source
Trigger event
Start
Duration
Actions
app
58246
58246
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
18m 40s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
08ff215
JY-20606: desktop app ui
Push
Commit pushed
Copy timestamp to clipboard
6m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job checkout-code
checkout-code
885153
1m 40s
1m 40s
SUCCESS job build-frontend
build-frontend
885156
1m 58s
1m 58s
SUCCESS job test-frontend
test-frontend
885158
2m 18s
2m 18s
SUCCESS job build-backend
build-backend
885154
1m 10s
1m 10s
SUCCESS job phpstan
phpstan
885163
1m 9s
1m 9s
SUCCESS job prepare_deploy_revision_subenv
prepare_deploy_revision_subenv
885159
53s
53s
RUNNING job build_docker_backend_code_subenv
build_docker_backend_code_subenv
885160
1m 23s
1m 23s
RUNNING job build_docker_worker_code_subenv
build_docker_worker_code_subenv
885165
1m 21s
1m 21s
RUNNING job build_docker_worker_video_code_subenv
build_docker_worker_video_code_subenv
885161
1m 22s
1m 22s
db_migrations_subenv
885167
deploy_docker_backend_code_subenv
885169
deploy_docker_worker_code_subenv
885170
deploy_docker_worker_video_code_subenv
885168
deploy_frontend_assets_to_s3_subenv
885166
SUCCESS job setup
setup
885157
1m 26s
1m 26s
RUNNING job test
test
885162
1m 11s
1m 11s
RUNNING job test-backend-lint
test-backend-lint
885155
3m 31s
3m 31s
sonar_cloud
885164
SUCCESS workflow setup-workflow. Collapse the workflow jobs list.
Status Passed Success
Success
setup-workflow
setup-workflow
SETUP
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
08ff215
JY-20606: desktop app ui
Push
Commit pushed
Copy timestamp to clipboard
7m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job setup
setup
885152
34s
34s
app
58245
58245
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
17m 56s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Open commit on version control site
5b2c922
Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking
Push...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.057063047,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.0028257978,"top":0.08060654,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.015957447,"top":0.09217877,"width":0.40492022,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"CloudWatch | us-east-2","depth":4,"bounds":{"left":0.0028257978,"top":0.11332801,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CloudWatch | us-east-2","depth":5,"bounds":{"left":0.015957447,"top":0.12490024,"width":0.04138963,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.15123703,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"bounds":{"left":0.0028257978,"top":0.17478053,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"bounds":{"left":0.015957447,"top":0.18635276,"width":0.16888298,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"bounds":{"left":0.0,"top":0.207502,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.21907422,"width":0.16140293,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.24022347,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.25179568,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.27294493,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.28451717,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.3056664,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.31723863,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.33838788,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.3499601,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.37110934,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.38268158,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.4038308,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.41540304,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0,"top":0.4365523,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.4481245,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.46927375,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.48084596,"width":0.15159574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Data Explorer","depth":4,"bounds":{"left":0.0,"top":0.5019952,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Data Explorer","depth":5,"bounds":{"left":0.013297873,"top":0.51356745,"width":0.0234375,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.53471667,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.5462889,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.5674381,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.57901037,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.60015965,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.6117318,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.6328811,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.6444533,"width":0.09524601,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Team - Backlog - Jira","depth":4,"bounds":{"left":0.0,"top":0.66560256,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team - Backlog - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.6771748,"width":0.053025264,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.698324,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.70989627,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.7310455,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.7426177,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.76376694,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.7753392,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.7709497,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.7980846,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Go to home page","depth":9,"bounds":{"left":0.08726729,"top":0.061452515,"width":0.044215426,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Auto theme","depth":9,"bounds":{"left":0.9375,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open notifications","depth":9,"bounds":{"left":0.95212764,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Open support menu","depth":9,"bounds":{"left":0.96675533,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Open user menu","depth":9,"bounds":{"left":0.98138297,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"org avatar Current organization: jiminny","depth":9,"bounds":{"left":0.08693484,"top":0.10295291,"width":0.01462766,"height":0.035115723},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Home","depth":10,"bounds":{"left":0.08494016,"top":0.15083799,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home","depth":12,"bounds":{"left":0.087765954,"top":0.1839585,"width":0.012965426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pipelines","depth":10,"bounds":{"left":0.08494016,"top":0.21308859,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines","depth":12,"bounds":{"left":0.083942816,"top":0.2462091,"width":0.020611702,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Projects","depth":10,"bounds":{"left":0.08494016,"top":0.2753392,"width":0.01861702,"height":0.04668795},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Projects","depth":12,"bounds":{"left":0.0852726,"top":0.3084597,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Deploys","depth":10,"bounds":{"left":0.08494016,"top":0.33798882,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Deploys","depth":12,"bounds":{"left":0.08543883,"top":0.37071028,"width":0.01761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":10,"bounds":{"left":0.08494016,"top":0.40023944,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":12,"bounds":{"left":0.085605055,"top":0.4329609,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Runners","depth":10,"bounds":{"left":0.08494016,"top":0.46249002,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Runners","depth":12,"bounds":{"left":0.0852726,"top":0.49561054,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Org","depth":10,"bounds":{"left":0.08494016,"top":0.52474064,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Org","depth":12,"bounds":{"left":0.090259306,"top":0.55786115,"width":0.007978723,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Plan","depth":10,"bounds":{"left":0.08494016,"top":0.58699125,"width":0.01861702,"height":0.04668795},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Plan","depth":12,"bounds":{"left":0.08959442,"top":0.6201117,"width":0.00930851,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chunk sidecars","depth":11,"bounds":{"left":0.07962101,"top":0.8591381,"width":0.02925532,"height":0.059457302},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Chunk sidecars","depth":13,"bounds":{"left":0.08494016,"top":0.8922586,"width":0.01861702,"height":0.026735835},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PREVIEW","depth":12,"bounds":{"left":0.08743351,"top":0.8567438,"width":0.013630319,"height":0.009177973},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chunk","depth":10,"bounds":{"left":0.07962101,"top":0.9345571,"width":0.02925532,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Chunk","depth":12,"bounds":{"left":0.08726729,"top":0.96727854,"width":0.013962766,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Smarter Testing is now in beta","depth":9,"bounds":{"left":0.1221742,"top":0.8707103,"width":0.10638298,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Smarter Testing is now in beta","depth":10,"bounds":{"left":0.1221742,"top":0.87031126,"width":0.07795878,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Get faster feedback on your tests while maintaining full confidence","depth":10,"bounds":{"left":0.1221742,"top":0.8934557,"width":0.098902926,"height":0.030327214},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss","depth":9,"bounds":{"left":0.16156915,"top":0.93615323,"width":0.027925532,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Get Started","depth":9,"bounds":{"left":0.19215426,"top":0.93615323,"width":0.036402926,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Get Started","depth":10,"bounds":{"left":0.1974734,"top":0.94213885,"width":0.025764627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Dashboard All Pipelines","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All Pipelines","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Project Outline app","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"app","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Overview","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Overview","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Deploys","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Deploys","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Lightning Manage triggers","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Manage triggers","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Trigger Pipeline","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pipelines All pipelines my-pipelines-filter","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All pipelines","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"app Project Filter. Selected \"app\"","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"All branches Branch Filter. Selected \"All branches\"","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All branches","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Start Time Cutoff date Arrow Drop Down","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Cutoff date","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"All statuses Arrow Drop Down","depth":13,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"statuses","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filter Display options","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Display options","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Pipeline","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Status","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Workflow","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Checkout source","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Trigger event","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Start","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Duration","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Actions","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"58246","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"58246","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Status Running Running","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18m 40s","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remain","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Info Outline","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"build_accept_deploy","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_accept_deploy","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20606-desktop-app-recall","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20606-desktop-app-recall","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Open commit on version control site","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"08ff215","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20606: desktop app ui","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Push","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commit pushed","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp to clipboard","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"6m ago","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp duration to clipboard","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from start","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from failed","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Cancel workflow","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Fix workflow","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"More Actions","depth":12,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Jobs","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job checkout-code","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"checkout-code","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885153","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 40s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 40s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job build-frontend","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build-frontend","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885156","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 58s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 58s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job test-frontend","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"test-frontend","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885158","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2m 18s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2m 18s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job build-backend","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build-backend","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885154","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 10s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 10s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job phpstan","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"phpstan","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885163","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 9s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 9s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job prepare_deploy_revision_subenv","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"prepare_deploy_revision_subenv","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885159","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"53s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"53s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job build_docker_backend_code_subenv","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_docker_backend_code_subenv","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885160","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 23s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 23s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job build_docker_worker_code_subenv","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_docker_worker_code_subenv","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885165","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 21s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 21s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job build_docker_worker_video_code_subenv","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_docker_worker_video_code_subenv","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885161","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 22s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 22s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"db_migrations_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.04920213,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885167","depth":16,"bounds":{"left":0.35472074,"top":0.0,"width":0.016289894,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_docker_backend_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.083942816,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885169","depth":16,"bounds":{"left":0.38946143,"top":0.0,"width":0.016456118,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_docker_worker_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.079953454,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885170","depth":16,"bounds":{"left":0.3854721,"top":0.0,"width":0.016289894,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_docker_worker_video_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.09375,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885168","depth":16,"bounds":{"left":0.39926863,"top":0.0,"width":0.016456118,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_frontend_assets_to_s3_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.025139665,"width":0.083942816,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885166","depth":16,"bounds":{"left":0.38946143,"top":0.025139665,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job setup","depth":14,"bounds":{"left":0.28756648,"top":0.048284117,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"setup","depth":15,"bounds":{"left":0.30285904,"top":0.050678372,"width":0.012632979,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885157","depth":16,"bounds":{"left":0.3181516,"top":0.050678372,"width":0.016123671,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 26s","depth":14,"bounds":{"left":0.90226066,"top":0.048284117,"width":0.016289894,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 26s","depth":15,"bounds":{"left":0.90226066,"top":0.050678372,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job test","depth":14,"bounds":{"left":0.28756648,"top":0.073822826,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"test","depth":15,"bounds":{"left":0.30285904,"top":0.07621708,"width":0.008643617,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885162","depth":16,"bounds":{"left":0.31416222,"top":0.07621708,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 11s","depth":14,"bounds":{"left":0.9037567,"top":0.073822826,"width":0.014793883,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 11s","depth":15,"bounds":{"left":0.9037567,"top":0.07621708,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job test-backend-lint","depth":14,"bounds":{"left":0.28756648,"top":0.09936153,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"test-backend-lint","depth":15,"bounds":{"left":0.30285904,"top":0.10175578,"width":0.03873005,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885155","depth":16,"bounds":{"left":0.34424868,"top":0.10175578,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"3m 31s","depth":14,"bounds":{"left":0.9020944,"top":0.09936153,"width":0.016456118,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"3m 31s","depth":15,"bounds":{"left":0.9020944,"top":0.10175578,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sonar_cloud","depth":15,"bounds":{"left":0.30285904,"top":0.1272945,"width":0.02642952,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885164","depth":16,"bounds":{"left":0.33194813,"top":0.1272945,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"SUCCESS workflow setup-workflow. Collapse the workflow jobs list.","depth":13,"bounds":{"left":0.27759308,"top":0.16799681,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Status Passed Success","depth":12,"bounds":{"left":0.28956118,"top":0.16959298,"width":0.033410903,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Success","depth":13,"bounds":{"left":0.30019948,"top":0.17398244,"width":0.018783245,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"setup-workflow","depth":12,"bounds":{"left":0.3444149,"top":0.17278531,"width":0.03507314,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"setup-workflow","depth":13,"bounds":{"left":0.3444149,"top":0.1735834,"width":0.03507314,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SETUP","depth":12,"bounds":{"left":0.38480717,"top":0.17557861,"width":0.011136968,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20606-desktop-app-recall","depth":13,"bounds":{"left":0.5728058,"top":0.16640064,"width":0.06732048,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20606-desktop-app-recall","depth":14,"bounds":{"left":0.5728058,"top":0.16719872,"width":0.06732048,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Open commit on version control site","depth":14,"bounds":{"left":0.5728058,"top":0.18515563,"width":0.08228058,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"08ff215","depth":16,"bounds":{"left":0.5728058,"top":0.1867518,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20606: desktop app ui","depth":16,"bounds":{"left":0.59142286,"top":0.1867518,"width":0.057679523,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Push","depth":13,"bounds":{"left":0.7190825,"top":0.1735834,"width":0.011303191,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commit pushed","depth":14,"bounds":{"left":0.73171544,"top":0.1735834,"width":0.034574468,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp to clipboard","depth":13,"bounds":{"left":0.85106385,"top":0.16640064,"width":0.027593086,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7m ago","depth":15,"bounds":{"left":0.85671544,"top":0.17517957,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp duration to clipboard","depth":13,"bounds":{"left":0.90392286,"top":0.16640064,"width":0.019946808,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from start","depth":12,"bounds":{"left":0.92918885,"top":0.16959298,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from failed","depth":12,"bounds":{"left":0.93982714,"top":0.16959298,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Cancel workflow","depth":12,"bounds":{"left":0.95046544,"top":0.16959298,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Fix workflow","depth":12,"bounds":{"left":0.96110374,"top":0.16959298,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"More Actions","depth":12,"bounds":{"left":0.97174203,"top":0.16959298,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Jobs","depth":14,"bounds":{"left":0.26080453,"top":0.22705507,"width":0.010804521,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job setup","depth":14,"bounds":{"left":0.28756648,"top":0.22426178,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"setup","depth":15,"bounds":{"left":0.30285904,"top":0.22705507,"width":0.012632979,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885152","depth":16,"bounds":{"left":0.3181516,"top":0.22705507,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"34s","depth":14,"bounds":{"left":0.9099069,"top":0.22426178,"width":0.008643617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"34s","depth":15,"bounds":{"left":0.9099069,"top":0.22705507,"width":0.008643617,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app","depth":13,"bounds":{"left":0.12283909,"top":0.27653632,"width":0.00831117,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"58245","depth":12,"bounds":{"left":0.12283909,"top":0.29289705,"width":0.014295213,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"58245","depth":13,"bounds":{"left":0.12283909,"top":0.29369512,"width":0.014295213,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.","depth":13,"bounds":{"left":0.27759308,"top":0.27853152,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Status Running Running","depth":12,"bounds":{"left":0.28956118,"top":0.2801277,"width":0.03274601,"height":0.023144454},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":13,"bounds":{"left":0.30019948,"top":0.28451717,"width":0.018118352,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"17m 56s","depth":13,"bounds":{"left":0.28956118,"top":0.30885875,"width":0.018284574,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remain","depth":13,"bounds":{"left":0.30784574,"top":0.30885875,"width":0.016123671,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Info Outline","depth":12,"bounds":{"left":0.3246343,"top":0.30327216,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"build_accept_deploy","depth":12,"bounds":{"left":0.3444149,"top":0.28332004,"width":0.045545213,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_accept_deploy","depth":13,"bounds":{"left":0.3444149,"top":0.28451717,"width":0.045545213,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":13,"bounds":{"left":0.5728058,"top":0.27693537,"width":0.11768617,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":14,"bounds":{"left":0.5728058,"top":0.27773345,"width":0.11768617,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Open commit on version control site","depth":14,"bounds":{"left":0.5728058,"top":0.29569036,"width":0.12765957,"height":0.033519555},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5b2c922","depth":16,"bounds":{"left":0.5728058,"top":0.2972865,"width":0.019946808,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking","depth":16,"bounds":{"left":0.5728058,"top":0.2972865,"width":0.11319814,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Push","depth":13,"bounds":{"left":0.7190825,"top":0.28451717,"width":0.011303191,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-7236633689412449760
|
-4321403799718891360
|
idle
|
accessibility
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Go to home page
Auto theme
Open notifications
Open support menu
Open user menu
org avatar Current organization: jiminny
Home
Home
Pipelines
Pipelines
Projects
Projects
Deploys
Deploys
Insights
Insights
Runners
Runners
Org
Org
Plan
Plan
Chunk sidecars
Chunk sidecars
PREVIEW
Chunk
Chunk
Smarter Testing is now in beta
Smarter Testing is now in beta
Get faster feedback on your tests while maintaining full confidence
Dismiss
Get Started
Get Started
Dashboard All Pipelines
All Pipelines
Project Outline app
app
app
app
Overview
Overview
Settings
Settings
Deploys
Deploys
Lightning Manage triggers
Manage triggers
Trigger Pipeline
Pipelines All pipelines my-pipelines-filter
All pipelines
app Project Filter. Selected "app"
app
All branches Branch Filter. Selected "All branches"
All branches
Start Time Cutoff date Arrow Drop Down
Cutoff date
All statuses Arrow Drop Down
All
statuses
Filter Display options
Display options
Pipeline
Status
Workflow
Checkout source
Trigger event
Start
Duration
Actions
app
58246
58246
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
18m 40s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
08ff215
JY-20606: desktop app ui
Push
Commit pushed
Copy timestamp to clipboard
6m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job checkout-code
checkout-code
885153
1m 40s
1m 40s
SUCCESS job build-frontend
build-frontend
885156
1m 58s
1m 58s
SUCCESS job test-frontend
test-frontend
885158
2m 18s
2m 18s
SUCCESS job build-backend
build-backend
885154
1m 10s
1m 10s
SUCCESS job phpstan
phpstan
885163
1m 9s
1m 9s
SUCCESS job prepare_deploy_revision_subenv
prepare_deploy_revision_subenv
885159
53s
53s
RUNNING job build_docker_backend_code_subenv
build_docker_backend_code_subenv
885160
1m 23s
1m 23s
RUNNING job build_docker_worker_code_subenv
build_docker_worker_code_subenv
885165
1m 21s
1m 21s
RUNNING job build_docker_worker_video_code_subenv
build_docker_worker_video_code_subenv
885161
1m 22s
1m 22s
db_migrations_subenv
885167
deploy_docker_backend_code_subenv
885169
deploy_docker_worker_code_subenv
885170
deploy_docker_worker_video_code_subenv
885168
deploy_frontend_assets_to_s3_subenv
885166
SUCCESS job setup
setup
885157
1m 26s
1m 26s
RUNNING job test
test
885162
1m 11s
1m 11s
RUNNING job test-backend-lint
test-backend-lint
885155
3m 31s
3m 31s
sonar_cloud
885164
SUCCESS workflow setup-workflow. Collapse the workflow jobs list.
Status Passed Success
Success
setup-workflow
setup-workflow
SETUP
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
08ff215
JY-20606: desktop app ui
Push
Commit pushed
Copy timestamp to clipboard
7m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job setup
setup
885152
34s
34s
app
58245
58245
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
17m 56s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Open commit on version control site
5b2c922
Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking
Push...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17436
|
774
|
19
|
2026-05-11T10:23:08.188412+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778494988188_m2.jpg...
|
Firefox
|
Screenpipe — Archive — Personal
|
True
|
app.screenpipe.lakylak.xyz
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rireroxcalMISTOMbookmarksProtllesrTavsco.sProletey rireroxcalMISTOMbookmarksProtllesrTavsco.sProletey© BatchSyncCollectole balchsynckealssec clientonec closeaDealstagess @ ermactiviiyservice.phgDealrielasservice.gc)Decorateacuivilv.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterc) Hubspotlokenman© PayloadBuilder.phpC) RemotecrmobiectrP ResponseNormalizec) Service,onoC)SvncFieldAction.onC) SvncRelatedActivitC) WebhookSvncBatclv MintearationAorM AcceccorsConfigD DTO• M SiltersD Jobs• M ProcnectSoarchStreW sevice lraits© DataClient.php© DecorateActivity.ph(e)LocalSearch.onp• LocalSearchInterfa© RemoteSearch.phpc) Service.phpv D Listeners© ConvertLeadActivitc) PurceLookuocache> M Metadata> Miarationi> M Pipedrivev Salesforce• D FieldsM OnnortunitvMatcheMOnnortunitvSvneStM ProsneetSearchStr.• M ServiceTraitcC) Client nhr© DecorateActivity.ph. Delete@biectsTrait© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php137139149Windowmelp..••)(C) TrackAutomated ReporiGeneratedeventonp© PlaybackController.phpC) Hubspot/Service.php© HubSp* New Tabhudspotsynestrategybase.ongT SyncCrmEntitiesTrait.phpCachedCrmServiceDecorator.php© ProspectCache.php© JiminnyDebugComm@ Screenpipe - Archive(C) CheckAndRetrvRemoteMatch.pnpClient.phpxpnp_ide_nelper.ong©MatchCrmData.pnp© Crmol- New Tabclass Cllent extends Baseclient 1mpLements Hubspotclientintertaceououc tunction 1shubspotrateuimithrowable sel: 000lif Se instanceof BadReauestI1 $e instanceof DealApiExceptionSe instanceof ContactAniExcentionI1 $e instanceof CompanyApiExceptionSe instanceofGuzzleltto Exceotion ReauestExcentionneturn (int) So->aetCode0 === 429:• app.screenpipe.lakylak.xyzScreenpipe (archive.db - 12323.6MB)TOTAL SPAN12h 5m09:45 → 21:506262PhoStormiTerm2FirefoxClaudeSlackCleanShot XAlfredMusicSindorQuickTime PlayerRaycastControl CentreOnozml6h 56m1h 35m1h 31m58mSustem InformationActivitySearch3.1n14ml13• suppont Dally • In 1h 31 m100% 2Mon 11 May 13:23:07Work ReportuinnleclolleAl Summary07 / 05 / 2026S1: 09:45-12:24 12h 29m)S2: 13:07-16:41(3h 34mlS2: 17:00-21:29 (4h 29m)S4: 21:50-21:50 (<1mllS2 3h 34mS3 4h 39m09:45 → 21:50• public function parseRetryAfter(Throwable Se): intIlluminate\Support\Facades\Loq::channel( channel: 'custom_channel')→>info('$e ' . PHP EOL . print_r(Se.return: true)return 5if (method exists(Se.method: 'getResponseHeaders')) {sheaders = se->qetkesponsereaderso?:Svalue = Sheadersf 'Retry-After'1 22 Sheadersf'retry-after'] 22 null:1t is arravsvalue)) 3Svalue = Svaluelol 22 null.if (is numeric(Svalue)) {return (int) svalue:Snolicv = Sthis->nansePolicv(Se)•if (Snoliey === ITEN SECONDLY ROLLING') {neturn 10.if (Spolicy === 'SECONDLY) {return 1;if (Spolicy === 'DAILY LIMIT') {notunn kad.Sthis->loq->warning('[Hubspot] No retry-after header or policy name found, using default'. ['exception class' => get class(Se).1:UI EVENTS7412lminnv.atlassian.net/ira/software/c/oroiects/JY/boards/37lminnv.atlassian.net//iira/software/c/oroiects/Y/boards/372selectedissue=1Y-20625aon.staaing.liminnv.com/dashboardmeet aoocle com/landine?authucer-lukas. [EMAIL]/issues/7007366572/?environment=production&environment=production-eu&project=82419&query=is%3Aunresolved&referrer=issue-stream&meet.google.com/axk-zwsm-vok?authuser=lukas.kovaliK/40jiminny.commeet.google.com/aqt-teir-cwt?authuser=lukas.kovalik%40iminny.comlocin.microsoftonline.com/common/SaS/ProcessAuthliminnv.atlassian.net/iira/software/c/oroiects/JY/boards/372selected|ssue=JY-20725htto:[IP_ADDRESS]:8766develoners.hubsnot.com/docs/ani-reference/latest/crm/associations/associate-records/quide#limitsdennikn.skapp.dev.jiminny.com/loginmnucernilot iolevents/tracked_event/25developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm#limitshpanel.hostinger.com/domain/lakvlak.xvz/domain-overview?olacement=Businesspredolatne.dennikn.sk/pavments/pavments/mv?_fid=be34liminnv.atlassian.net/iira/software/c/oroiects/JY/boards/372selected|ssue=JY-20352predolatne.dennikn.sk/oavments/pavments/recurrent-stop?recurrentPavmentld=3297926nrednlatne dennikn sklnavmentc/navmentc/mvhnanel hostinaer coml?redirect back url-httnc-llauth.hoctinger.com/loain8 ca-GA11 2126150572 1777212197hpanel.hostinger.comgithub.com//iminnylapp/pull//zuz4/changes...
|
NULL
|
-7234318632600020530
|
NULL
|
click
|
ocr
|
NULL
|
rireroxcalMISTOMbookmarksProtllesrTavsco.sProletey rireroxcalMISTOMbookmarksProtllesrTavsco.sProletey© BatchSyncCollectole balchsynckealssec clientonec closeaDealstagess @ ermactiviiyservice.phgDealrielasservice.gc)Decorateacuivilv.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterc) Hubspotlokenman© PayloadBuilder.phpC) RemotecrmobiectrP ResponseNormalizec) Service,onoC)SvncFieldAction.onC) SvncRelatedActivitC) WebhookSvncBatclv MintearationAorM AcceccorsConfigD DTO• M SiltersD Jobs• M ProcnectSoarchStreW sevice lraits© DataClient.php© DecorateActivity.ph(e)LocalSearch.onp• LocalSearchInterfa© RemoteSearch.phpc) Service.phpv D Listeners© ConvertLeadActivitc) PurceLookuocache> M Metadata> Miarationi> M Pipedrivev Salesforce• D FieldsM OnnortunitvMatcheMOnnortunitvSvneStM ProsneetSearchStr.• M ServiceTraitcC) Client nhr© DecorateActivity.ph. Delete@biectsTrait© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php137139149Windowmelp..••)(C) TrackAutomated ReporiGeneratedeventonp© PlaybackController.phpC) Hubspot/Service.php© HubSp* New Tabhudspotsynestrategybase.ongT SyncCrmEntitiesTrait.phpCachedCrmServiceDecorator.php© ProspectCache.php© JiminnyDebugComm@ Screenpipe - Archive(C) CheckAndRetrvRemoteMatch.pnpClient.phpxpnp_ide_nelper.ong©MatchCrmData.pnp© Crmol- New Tabclass Cllent extends Baseclient 1mpLements Hubspotclientintertaceououc tunction 1shubspotrateuimithrowable sel: 000lif Se instanceof BadReauestI1 $e instanceof DealApiExceptionSe instanceof ContactAniExcentionI1 $e instanceof CompanyApiExceptionSe instanceofGuzzleltto Exceotion ReauestExcentionneturn (int) So->aetCode0 === 429:• app.screenpipe.lakylak.xyzScreenpipe (archive.db - 12323.6MB)TOTAL SPAN12h 5m09:45 → 21:506262PhoStormiTerm2FirefoxClaudeSlackCleanShot XAlfredMusicSindorQuickTime PlayerRaycastControl CentreOnozml6h 56m1h 35m1h 31m58mSustem InformationActivitySearch3.1n14ml13• suppont Dally • In 1h 31 m100% 2Mon 11 May 13:23:07Work ReportuinnleclolleAl Summary07 / 05 / 2026S1: 09:45-12:24 12h 29m)S2: 13:07-16:41(3h 34mlS2: 17:00-21:29 (4h 29m)S4: 21:50-21:50 (<1mllS2 3h 34mS3 4h 39m09:45 → 21:50• public function parseRetryAfter(Throwable Se): intIlluminate\Support\Facades\Loq::channel( channel: 'custom_channel')→>info('$e ' . PHP EOL . print_r(Se.return: true)return 5if (method exists(Se.method: 'getResponseHeaders')) {sheaders = se->qetkesponsereaderso?:Svalue = Sheadersf 'Retry-After'1 22 Sheadersf'retry-after'] 22 null:1t is arravsvalue)) 3Svalue = Svaluelol 22 null.if (is numeric(Svalue)) {return (int) svalue:Snolicv = Sthis->nansePolicv(Se)•if (Snoliey === ITEN SECONDLY ROLLING') {neturn 10.if (Spolicy === 'SECONDLY) {return 1;if (Spolicy === 'DAILY LIMIT') {notunn kad.Sthis->loq->warning('[Hubspot] No retry-after header or policy name found, using default'. ['exception class' => get class(Se).1:UI EVENTS7412lminnv.atlassian.net/ira/software/c/oroiects/JY/boards/37lminnv.atlassian.net//iira/software/c/oroiects/Y/boards/372selectedissue=1Y-20625aon.staaing.liminnv.com/dashboardmeet aoocle com/landine?authucer-lukas. [EMAIL]/issues/7007366572/?environment=production&environment=production-eu&project=82419&query=is%3Aunresolved&referrer=issue-stream&meet.google.com/axk-zwsm-vok?authuser=lukas.kovaliK/40jiminny.commeet.google.com/aqt-teir-cwt?authuser=lukas.kovalik%40iminny.comlocin.microsoftonline.com/common/SaS/ProcessAuthliminnv.atlassian.net/iira/software/c/oroiects/JY/boards/372selected|ssue=JY-20725htto:[IP_ADDRESS]:8766develoners.hubsnot.com/docs/ani-reference/latest/crm/associations/associate-records/quide#limitsdennikn.skapp.dev.jiminny.com/loginmnucernilot iolevents/tracked_event/25developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm#limitshpanel.hostinger.com/domain/lakvlak.xvz/domain-overview?olacement=Businesspredolatne.dennikn.sk/pavments/pavments/mv?_fid=be34liminnv.atlassian.net/iira/software/c/oroiects/JY/boards/372selected|ssue=JY-20352predolatne.dennikn.sk/oavments/pavments/recurrent-stop?recurrentPavmentld=3297926nrednlatne dennikn sklnavmentc/navmentc/mvhnanel hostinaer coml?redirect back url-httnc-llauth.hoctinger.com/loain8 ca-GA11 2126150572 1777212197hpanel.hostinger.comgithub.com//iminnylapp/pull//zuz4/changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
21250
|
931
|
7
|
2026-05-11T17:32:19.590953+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778520739590_m2.jpg...
|
iTerm2
|
-zsh
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sun May 10 21:10:26 on ttys013
Poetry Last login: Sun May 10 21:10:26 on ttys013
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
-zsh...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sun May 10 21:10:26 on ttys013\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe \nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4787234,"height":-0.06304872},"on_screen":true,"value":"Last login: Sun May 10 21:10:26 on ttys013\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe \nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"-zsh","depth":1,"bounds":{"left":0.50398934,"top":1.0,"width":0.010970744,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7233334204856505338
|
-4070517209923662823
|
click
|
accessibility
|
NULL
|
Last login: Sun May 10 21:10:26 on ttys013
Poetry Last login: Sun May 10 21:10:26 on ttys013
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
-zsh...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
21252
|
930
|
8
|
2026-05-11T17:32:26.220940+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778520746220_m1.jpg...
|
iTerm2
|
-zsh
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sun May 10 21:10:26 on ttys013
Poetry Last login: Sun May 10 21:10:26 on ttys013
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
-zsh...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sun May 10 21:10:26 on ttys013\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe \nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":1.0,"height":0.9122222},"on_screen":true,"lines":[{"char_start":0,"char_count":43,"bounds":{"left":0.0034722222,"top":0.08777778,"width":0.23888889,"height":0.02}},{"char_start":43,"char_count":1,"bounds":{"left":0.0034722222,"top":0.107777774,"width":0.0055555557,"height":0.02}},{"char_start":44,"char_count":75,"bounds":{"left":0.0034722222,"top":0.12777779,"width":0.41666666,"height":0.02}},{"char_start":119,"char_count":1,"bounds":{"left":0.0034722222,"top":0.14777778,"width":0.0055555557,"height":0.02}},{"char_start":120,"char_count":75,"bounds":{"left":0.0034722222,"top":0.16777778,"width":0.41666666,"height":0.02}},{"char_start":195,"char_count":91,"bounds":{"left":0.0034722222,"top":0.18777777,"width":0.50555557,"height":0.02}},{"char_start":286,"char_count":65,"bounds":{"left":0.0034722222,"top":0.20777778,"width":0.3611111,"height":0.02}},{"char_start":351,"char_count":44,"bounds":{"left":0.0034722222,"top":0.22777778,"width":0.24444444,"height":0.02}}],"value":"Last login: Sun May 10 21:10:26 on ttys013\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe \nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"-zsh","depth":1,"bounds":{"left":0.48819444,"top":0.033333335,"width":0.022916667,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7233334204856505338
|
-4070517209923662823
|
click
|
accessibility
|
NULL
|
Last login: Sun May 10 21:10:26 on ttys013
Poetry Last login: Sun May 10 21:10:26 on ttys013
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
-zsh...
|
21251
|
NULL
|
NULL
|
NULL
|
|
21253
|
931
|
8
|
2026-05-11T17:32:26.220939+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778520746220_m2.jpg...
|
iTerm2
|
-zsh
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sun May 10 21:10:26 on ttys013
Poetry Last login: Sun May 10 21:10:26 on ttys013
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
-zsh...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sun May 10 21:10:26 on ttys013\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe \nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4787234,"height":-0.06304872},"on_screen":true,"value":"Last login: Sun May 10 21:10:26 on ttys013\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe \nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"-zsh","depth":1,"bounds":{"left":0.50398934,"top":1.0,"width":0.010970744,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7233334204856505338
|
-4070517209923662823
|
click
|
accessibility
|
NULL
|
Last login: Sun May 10 21:10:26 on ttys013
Poetry Last login: Sun May 10 21:10:26 on ttys013
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
Poetry could not find a pyproject.toml file in /Users/lukas or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopy
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ open ~/.screenpipe
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
-zsh...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10987
|
492
|
16
|
2026-05-08T18:11:24.830518+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778263884830_m2.jpg...
|
Code
|
finance [SSH: nas]
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Start Debugging
F5
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"bounds":{"left":0.0,"top":0.047885075,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":true},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.057462092,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"bounds":{"left":0.0,"top":0.08619314,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.09577015,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G)","depth":19,"bounds":{"left":0.0,"top":0.1245012,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.13407822,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"bounds":{"left":0.0,"top":0.16280925,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.17238627,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"bounds":{"left":0.0,"top":0.20111732,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.21069433,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X)","depth":19,"bounds":{"left":0.0,"top":0.23942538,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.2490024,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Containers","depth":19,"bounds":{"left":0.0,"top":0.27773345,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"EXPLORER","depth":17,"bounds":{"left":0.022606382,"top":0.047885075,"width":0.018949468,"height":0.02793296},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"EXPLORER","depth":18,"bounds":{"left":0.022606382,"top":0.056664005,"width":0.018949468,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.056664005,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.024933511,"top":0.056664005,"width":0.01662234,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Explorer Section: finance [SSH: nas]","depth":21,"bounds":{"left":0.015957447,"top":0.07581804,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Explorer Section: finance [SSH: nas]","depth":22,"bounds":{"left":0.022606382,"top":0.07581804,"width":0.039228722,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"FINANCE [SSH: NAS]","depth":23,"bounds":{"left":0.022606382,"top":0.079010375,"width":0.039228722,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.07980846,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":17,"bounds":{"left":0.024933511,"top":0.07980846,"width":0.036901597,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Outline Section","depth":21,"bounds":{"left":0.015957447,"top":0.0933759,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.09577015,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"OUTLINE","depth":22,"bounds":{"left":0.022606382,"top":0.0933759,"width":0.01662234,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"OUTLINE","depth":23,"bounds":{"left":0.022606382,"top":0.09736632,"width":0.01662234,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.09736632,"width":0.0029920214,"height":0.0103751}},{"char_start":1,"char_count":6,"bounds":{"left":0.025598405,"top":0.09736632,"width":0.013630319,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Timeline Section","depth":21,"bounds":{"left":0.015957447,"top":0.11093376,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.11332801,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"TIMELINE","depth":22,"bounds":{"left":0.022606382,"top":0.11093376,"width":0.01761968,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"TIMELINE","depth":23,"bounds":{"left":0.022606382,"top":0.114924185,"width":0.01761968,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.114924185,"width":0.0026595744,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.025265958,"top":0.114924185,"width":0.015292553,"height":0.0103751}}],"role_description":"text"},{"role":"AXStaticText","text":"Open Chat","depth":28,"bounds":{"left":0.51263297,"top":0.59936154,"width":0.023271276,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.51263297,"top":0.60015965,"width":0.003656915,"height":0.011971269}},{"char_start":1,"char_count":8,"bounds":{"left":0.5162899,"top":0.60015965,"width":0.019614361,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"bounds":{"left":0.5787899,"top":0.60015965,"width":0.0033244682,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5880984,"top":0.60015965,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"I","depth":29,"bounds":{"left":0.5984042,"top":0.60015965,"width":0.0013297872,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Show All Commands","depth":28,"bounds":{"left":0.51263297,"top":0.6201117,"width":0.044215426,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.51263297,"top":0.6209098,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.515625,"top":0.6209098,"width":0.041223403,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⇧","depth":29,"bounds":{"left":0.5787899,"top":0.6209098,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5880984,"top":0.6209098,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"P","depth":29,"bounds":{"left":0.59773934,"top":0.6209098,"width":0.0026595744,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Start Debugging","depth":28,"bounds":{"left":0.51263297,"top":0.64166003,"width":0.035904255,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.51263297,"top":0.64166003,"width":0.0029920214,"height":0.012769354}},{"char_start":1,"char_count":14,"bounds":{"left":0.515625,"top":0.64166003,"width":0.032912236,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"F5","depth":29,"bounds":{"left":0.59674203,"top":0.6424581,"width":0.0043218085,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.59674203,"top":0.6424581,"width":0.0023271276,"height":0.011173184}},{"char_start":1,"char_count":1,"bounds":{"left":0.5987367,"top":0.6424581,"width":0.0023271276,"height":0.011173184}}],"role_description":"text"},{"role":"AXButton","text":"remote SSH: nas","depth":16,"bounds":{"left":0.0006648936,"top":0.98244214,"width":0.028590426,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.0033244682,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SSH: nas","depth":17,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.017952127,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":7,"bounds":{"left":0.009973404,"top":0.9856345,"width":0.01462766,"height":0.011173184}}],"role_description":"text"},{"role":"AXButton","text":"No Problems","depth":16,"bounds":{"left":0.03025266,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.031914894,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.03723404,"top":0.9856345,"width":0.004986702,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.041888297,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.04720745,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"No Ports Forwarded","depth":16,"bounds":{"left":0.054521278,"top":0.98244214,"width":0.012632979,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.05618351,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.061502658,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"bounds":{"left":0.9886968,"top":0.98244214,"width":0.010638298,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"bounds":{"left":0.9650931,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.96675533,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.013962766,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":6,"bounds":{"left":0.9734042,"top":0.9856345,"width":0.010638298,"height":0.011173184}}],"role_description":"text"},{"role":"AXStaticText","text":"Info: Setting up SSH Host nas: Setting up SSH tunnel","depth":12,"on_screen":false,"role_description":"text"}]...
|
-7228136453884782733
|
-988320295579311250
|
click
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Start Debugging
F5
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10988
|
491
|
19
|
2026-05-08T18:11:24.890038+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778263884890_m1.jpg...
|
Code
|
finance [SSH: nas]
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Start Debugging
F5
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":true},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Containers","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"EXPLORER","depth":17,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"EXPLORER","depth":18,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Explorer Section: finance [SSH: nas]","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Explorer Section: finance [SSH: nas]","depth":22,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"FINANCE [SSH: NAS]","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Outline Section","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"OUTLINE","depth":22,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"OUTLINE","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Timeline Section","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"TIMELINE","depth":22,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"TIMELINE","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open Chat","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"I","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Show All Commands","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⇧","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"P","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Start Debugging","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"F5","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"remote SSH: nas","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SSH: nas","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"No Problems","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"No Ports Forwarded","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Info: Setting up SSH Host nas: Setting up SSH tunnel","depth":12,"on_screen":false,"role_description":"text"}]...
|
-7228136453884782733
|
-988320295579311250
|
click
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Start Debugging
F5
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel...
|
10985
|
NULL
|
NULL
|
NULL
|
|
24082
|
1008
|
11
|
2026-05-12T08:42:58.962236+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778575378962_m1.jpg...
|
Alfred
|
Alfred
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
aws lo
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"aws lo","depth":1,"bounds":{"left":0.26180556,"top":0.16777778,"width":0.4763889,"height":0.05888889},"on_screen":true,"value":"aws lo","help_text":"Alfred Search","role_description":"text field","is_enabled":true,"is_focused":true}]...
|
-7227615396437612735
|
-3721649735994532755
|
visual_change
|
hybrid
|
NULL
|
aws lo
SlackFileEditViewGoHistoryWindowHelpDOCKERD aws lo
SlackFileEditViewGoHistoryWindowHelpDOCKERDOCKER (-zsh)DEV (-zsh)O $2APP (-zsh)883EU ()-zshkibana{"type" : "log""@timestamp": "2026-05-11T19:54:53Z"asticsearch","tags" : ["warning""elh:9200/"},"data"], "pid":7, "message" : "Unable to revive connection: [URL_WITH_CREDENTIALS] "data"],"pid":7,"message" : "Noaws ||kibanaugins"{"type" : "log", "@timestamp""licensing"], "pid" :7,"message" : "Licen:asticsearch dueto Error:No LivingconnectickibanaA.AWS service1 {"type" : "log","@timestamp"ticsearch", "data"], "pid" :7, "message" : "[Connectiontrror]: getaaarinto ENUTFUUND elasticsearch elasticsearch: 9200"}1 {"type" : "log","@timestamp": "2026-05-11T19:54:54Z", "tags" : ["warning", "elasticsearch", "data"],"pid" :7,"message": "Unable to revive connection: [URL_WITH_CREDENTIALS] "2026-05-11T19:54:54Z", "tags" : ["warning", "elasticsearch", "data"],"pid":7,"message":"No livingconnections"}kibana1 {"type": "log""@timestamp": "2026-05-11T19:54:54Z","tags": ["error","plugins", "taskManager""taskManager"], "pid":7, "message": "Failed to poll for work: Error: NoLiving connections"}kibana1 {"type": "log", "@timestamp": "2026-05-11T19:54:57Z", "tags" : ["error","elasticsearch".,"data"], "pid" :7, "message" :"[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200*}kibana1 {"type" : "Log","@timestamp" : "2026-05-11T19:54:57Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "Unable to revive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:54:57Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "No living connections"}kibanaI {"type" : "log", "@timestamp" : "2026-05-11T19:54:57Z", "tags" : ["error"ins","taskManager", "taskManager"], "pid":7, "message": "Failed to poll for work: Error: NoLiving connections"}I {"type" : "log", "@timestamp" : "2026-05-11T19:54:59Z""tags" : ["error", "elasticsearch", "data"], "pid" :7, "message" : "[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200"}1 {"type" : "log","@timestamp" : "2026-05-11T19:55: 00Z","tags" : ["warning"asticsearch", "data"], "pid" :7, "message": "Unable to revive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:55 :00Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "No living connections"}1 {"type": "log", "@timestamp" : "2026-05-11T19:55:00Z", "tags" : ["error", "plug, "taskManager",, "taskManager"], "pid" :7, "message": "Failed to poll for work: Error: NoLiving connections"}unexpected EOFikas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $ActivityFilesLaterMoreladlSupport Daily • in 3 h 18 m100% C8• Tue 12 May 11:42:58ED→Describe what you are looking forJiminny ...# support8 348 iiminnv-x-integrati...• MessagesC FilesBookmarksMore v+# backend# bugs# confusion-clinic# curiosity _lab#engineeringMore28Toolssome old corruYesterday~ ied the process, andnassively delayed it.A few fixes were applied. I have increased thenumber of servers consuming activities, but it willstill take some time until all scheduled activities areprocessed and indexed.What that means - we were processing older data(since Jan 1st 26) but those activities compete withfreshly imported calls and conversations. And newcalls and conversations may be delayed a bit.Apart from the delay not issues with new activitiesara aynerted.m happened only on EU, all US datawas finished over the weekend.* 3Create and find workflows and appsToday ~Customize navigation barvasll vasilev 11:38 AM# sofia-office# support# thank-yous# the_people_of jimi...All EU data is reindexed.But it turned out that the US is still in progress. Itwas reported the indexing has finished over theweekend, but apparently it's not. I've increased thenumber of consumers, that should speed up the^ Direct messagesremaining data consumption. Some delays mayhappen until all data indexing is finished.&. Petko KashinskiH1Ro Steliyan Georgiev. Galya DimitrovaMessage #supporte. Aneliya Angelovaa. Stefka Stoyanova+€. Vasil Vasilev...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
25221
|
NULL
|
0
|
2026-05-12T10:53:29.740977+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778583209740_m2.jpg...
|
Firefox
|
Pipelines - jiminny/app — Work
|
True
|
app.circleci.com/pipelines/github/jiminny/app
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Close tab
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Go to home page
Auto theme
Open notifications
Open support menu
Open user menu
org avatar Current organization: jiminny
Home
Home
Pipelines
Pipelines
Projects
Projects
Deploys
Deploys
Insights
Insights
Runners
Runners
Org
Org
Plan
Plan
Chunk sidecars
Chunk sidecars
PREVIEW
Chunk
Chunk
Smarter Testing is now in beta
Smarter Testing is now in beta
Get faster feedback on your tests while maintaining full confidence
Dismiss
Get Started
Get Started
Dashboard All Pipelines
All Pipelines
Project Outline app
app
app
app
Overview
Overview
Settings
Settings
Deploys
Deploys
Lightning Manage triggers
Manage triggers
Trigger Pipeline
Pipelines All pipelines my-pipelines-filter
All pipelines
app Project Filter. Selected "app"
app
All branches Branch Filter. Selected "All branches"
All branches
Start Time Cutoff date Arrow Drop Down
Cutoff date
All statuses Arrow Drop Down
All
statuses
Filter Display options
Display options
Pipeline
Status
Workflow
Checkout source
Trigger event
Start
Duration
Actions
app
58250
58250
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
25m 1s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
13a63f7
JY-20606: add provider detection
Push
Commit pushed
Copy timestamp to clipboard
8s ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
RUNNING job checkout-code
checkout-code
885223
9s
9s
build-frontend
885224
test-frontend
885225
build-backend
885226
phpstan
885239
prepare_deploy_revision_subenv
885230
build_docker_backend_code_subenv
885231
build_docker_worker_code_subenv
885232
build_docker_worker_video_code_subenv
885233
db_migrations_subenv
885235
deploy_docker_backend_code_subenv
885237
deploy_docker_worker_code_subenv
885238
deploy_docker_worker_video_code_subenv
885236
deploy_frontend_assets_to_s3_subenv
885234
setup
885228
test
885229
test-backend-lint
885227
sonar_cloud
885240
SUCCESS workflow setup-workflow. Collapse the workflow jobs list.
Status Passed Success
Success
setup-workflow
setup-workflow
SETUP
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
13a63f7
JY-20606: add provider detection
Push
Commit pushed
Copy timestamp to clipboard
1m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job setup
setup
885222
47s
47s
app
58249
58249
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
18m 29s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Open commit on version control site
62861fa
Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking
Push
Commit pushed
Copy timestamp to clipboard
7m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job checkout-code
checkout-code
885213
1m 38s
1m 38s
SUCCESS job build-frontend
build-frontend
885216
1m 29s
1m 29s
SUCCESS job test-frontend
test-frontend
885218
1m 43s
1m 43s
SUCCESS job build-backend
build-backend
885214
1m 12s
1m 12s
SUCCESS job phpstan
phpstan
885219
2m 18s
2m 18s
SUCCESS job setup
setup
885217
59s
59s
RUNNING job test
test
885220
2m 22s...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.057063047,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.0028257978,"top":0.08060654,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.015957447,"top":0.09217877,"width":0.40492022,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"CloudWatch | us-east-2","depth":4,"bounds":{"left":0.0028257978,"top":0.11332801,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CloudWatch | us-east-2","depth":5,"bounds":{"left":0.015957447,"top":0.12490024,"width":0.04138963,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.15123703,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"bounds":{"left":0.0028257978,"top":0.17478053,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"bounds":{"left":0.015957447,"top":0.18635276,"width":0.16888298,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"bounds":{"left":0.0,"top":0.207502,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.21907422,"width":0.16140293,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.24022347,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.25179568,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.27294493,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.28451717,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.3056664,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.31723863,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.33838788,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.3499601,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.37110934,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.38268158,"width":0.1931516,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.4038308,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.41540304,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0,"top":0.4365523,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.4481245,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.46927375,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.48084596,"width":0.15159574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Data Explorer","depth":4,"bounds":{"left":0.0,"top":0.5019952,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Data Explorer","depth":5,"bounds":{"left":0.013297873,"top":0.51356745,"width":0.0234375,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.53471667,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.5462889,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.5674381,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.57901037,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.60015965,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.6117318,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.6328811,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.6444533,"width":0.09524601,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Team - Backlog - Jira","depth":4,"bounds":{"left":0.0,"top":0.66560256,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team - Backlog - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.6771748,"width":0.053025264,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.698324,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.70989627,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.7310455,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.7426177,"width":0.1931516,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.76376694,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.7753392,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.7709497,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.7964884,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.80806065,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.8308061,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Go to home page","depth":9,"bounds":{"left":0.08726729,"top":0.061452515,"width":0.044215426,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Auto theme","depth":9,"bounds":{"left":0.9375,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open notifications","depth":9,"bounds":{"left":0.95212764,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Open support menu","depth":9,"bounds":{"left":0.96675533,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Open user menu","depth":9,"bounds":{"left":0.98138297,"top":0.061452515,"width":0.013297873,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"org avatar Current organization: jiminny","depth":9,"bounds":{"left":0.08693484,"top":0.10295291,"width":0.01462766,"height":0.035115723},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Home","depth":10,"bounds":{"left":0.08494016,"top":0.15083799,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home","depth":12,"bounds":{"left":0.087765954,"top":0.1839585,"width":0.012965426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pipelines","depth":10,"bounds":{"left":0.08494016,"top":0.21308859,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines","depth":12,"bounds":{"left":0.083942816,"top":0.2462091,"width":0.020611702,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Projects","depth":10,"bounds":{"left":0.08494016,"top":0.2753392,"width":0.01861702,"height":0.04668795},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Projects","depth":12,"bounds":{"left":0.0852726,"top":0.3084597,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Deploys","depth":10,"bounds":{"left":0.08494016,"top":0.33798882,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Deploys","depth":12,"bounds":{"left":0.08543883,"top":0.37071028,"width":0.01761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":10,"bounds":{"left":0.08494016,"top":0.40023944,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":12,"bounds":{"left":0.085605055,"top":0.4329609,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Runners","depth":10,"bounds":{"left":0.08494016,"top":0.46249002,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Runners","depth":12,"bounds":{"left":0.0852726,"top":0.49561054,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Org","depth":10,"bounds":{"left":0.08494016,"top":0.52474064,"width":0.01861702,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Org","depth":12,"bounds":{"left":0.090259306,"top":0.55786115,"width":0.007978723,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Plan","depth":10,"bounds":{"left":0.08494016,"top":0.58699125,"width":0.01861702,"height":0.04668795},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Plan","depth":12,"bounds":{"left":0.08959442,"top":0.6201117,"width":0.00930851,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chunk sidecars","depth":11,"bounds":{"left":0.07962101,"top":0.8591381,"width":0.02925532,"height":0.059457302},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Chunk sidecars","depth":13,"bounds":{"left":0.08494016,"top":0.8922586,"width":0.01861702,"height":0.026735835},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PREVIEW","depth":12,"bounds":{"left":0.08743351,"top":0.8567438,"width":0.013630319,"height":0.009177973},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chunk","depth":10,"bounds":{"left":0.07962101,"top":0.9345571,"width":0.02925532,"height":0.046288908},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Chunk","depth":12,"bounds":{"left":0.08726729,"top":0.96727854,"width":0.013962766,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Smarter Testing is now in beta","depth":9,"bounds":{"left":0.1221742,"top":0.8707103,"width":0.10638298,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Smarter Testing is now in beta","depth":10,"bounds":{"left":0.1221742,"top":0.87031126,"width":0.07795878,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Get faster feedback on your tests while maintaining full confidence","depth":10,"bounds":{"left":0.1221742,"top":0.8934557,"width":0.098902926,"height":0.030327214},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss","depth":9,"bounds":{"left":0.16156915,"top":0.93615323,"width":0.027925532,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Get Started","depth":9,"bounds":{"left":0.19215426,"top":0.93615323,"width":0.036402926,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Get Started","depth":10,"bounds":{"left":0.1974734,"top":0.94213885,"width":0.025764627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Dashboard All Pipelines","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All Pipelines","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Project Outline app","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"app","depth":13,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Overview","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Overview","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Deploys","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Deploys","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Lightning Manage triggers","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Manage triggers","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Trigger Pipeline","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pipelines All pipelines my-pipelines-filter","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All pipelines","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"app Project Filter. Selected \"app\"","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"All branches Branch Filter. Selected \"All branches\"","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All branches","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Start Time Cutoff date Arrow Drop Down","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Cutoff date","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"All statuses Arrow Drop Down","depth":13,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"statuses","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filter Display options","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Display options","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Pipeline","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Status","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Workflow","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Checkout source","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Trigger event","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Start","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Duration","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Actions","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"58250","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"58250","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Status Running Running","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"25m 1s","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remain","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Info Outline","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"build_accept_deploy","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_accept_deploy","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20606-desktop-app-recall","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20606-desktop-app-recall","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Open commit on version control site","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"13a63f7","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20606: add provider detection","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Push","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commit pushed","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp to clipboard","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"8s ago","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp duration to clipboard","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from start","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from failed","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Cancel workflow","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Fix workflow","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"More Actions","depth":12,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Jobs","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job checkout-code","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"checkout-code","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885223","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"9s","depth":14,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9s","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"build-frontend","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885224","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"test-frontend","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885225","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"build-backend","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885226","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpstan","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885239","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prepare_deploy_revision_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.0709774,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885230","depth":16,"bounds":{"left":0.37649602,"top":0.0,"width":0.017121011,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"build_docker_backend_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.08028591,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885231","depth":16,"bounds":{"left":0.38580453,"top":0.0,"width":0.016456118,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"build_docker_worker_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.076296546,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885232","depth":16,"bounds":{"left":0.38181517,"top":0.0,"width":0.017287234,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"build_docker_worker_video_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.0,"width":0.09009308,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885233","depth":16,"bounds":{"left":0.3956117,"top":0.0,"width":0.017453458,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"db_migrations_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.02434158,"width":0.04920213,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885235","depth":16,"bounds":{"left":0.35472074,"top":0.02434158,"width":0.017121011,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_docker_backend_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.04988029,"width":0.083942816,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885237","depth":16,"bounds":{"left":0.38946143,"top":0.04988029,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_docker_worker_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.075418994,"width":0.079953454,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885238","depth":16,"bounds":{"left":0.3854721,"top":0.075418994,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_docker_worker_video_code_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.1009577,"width":0.09375,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885236","depth":16,"bounds":{"left":0.39926863,"top":0.1009577,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"deploy_frontend_assets_to_s3_subenv","depth":15,"bounds":{"left":0.30285904,"top":0.1264964,"width":0.083942816,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885234","depth":16,"bounds":{"left":0.38946143,"top":0.1264964,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"setup","depth":15,"bounds":{"left":0.30285904,"top":0.15203512,"width":0.012300532,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885228","depth":16,"bounds":{"left":0.31781915,"top":0.15203512,"width":0.017121011,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"test","depth":15,"bounds":{"left":0.30285904,"top":0.17757383,"width":0.008477394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885229","depth":16,"bounds":{"left":0.31399602,"top":0.17757383,"width":0.017121011,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"test-backend-lint","depth":15,"bounds":{"left":0.30285904,"top":0.20311253,"width":0.038231384,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885227","depth":16,"bounds":{"left":0.34375,"top":0.20311253,"width":0.016788565,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sonar_cloud","depth":15,"bounds":{"left":0.30285904,"top":0.22865124,"width":0.02642952,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885240","depth":16,"bounds":{"left":0.33194813,"top":0.22865124,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"SUCCESS workflow setup-workflow. Collapse the workflow jobs list.","depth":13,"bounds":{"left":0.27759308,"top":0.26935354,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Status Passed Success","depth":12,"bounds":{"left":0.28956118,"top":0.27094972,"width":0.033410903,"height":0.022745412},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Success","depth":13,"bounds":{"left":0.30019948,"top":0.2753392,"width":0.018783245,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"setup-workflow","depth":12,"bounds":{"left":0.3444149,"top":0.27414206,"width":0.03507314,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"setup-workflow","depth":13,"bounds":{"left":0.3444149,"top":0.27494013,"width":0.03507314,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SETUP","depth":12,"bounds":{"left":0.38480717,"top":0.27693537,"width":0.011136968,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20606-desktop-app-recall","depth":13,"bounds":{"left":0.5728058,"top":0.2677574,"width":0.06732048,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20606-desktop-app-recall","depth":14,"bounds":{"left":0.5728058,"top":0.26855546,"width":0.06732048,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Open commit on version control site","depth":14,"bounds":{"left":0.5728058,"top":0.28651237,"width":0.1008976,"height":0.016759777},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"13a63f7","depth":16,"bounds":{"left":0.5728058,"top":0.28810853,"width":0.018284574,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20606: add provider detection","depth":16,"bounds":{"left":0.592254,"top":0.28810853,"width":0.075465426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Push","depth":13,"bounds":{"left":0.7190825,"top":0.27494013,"width":0.011303191,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commit pushed","depth":14,"bounds":{"left":0.73171544,"top":0.27494013,"width":0.034574468,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp to clipboard","depth":13,"bounds":{"left":0.8515625,"top":0.2677574,"width":0.027094414,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m ago","depth":15,"bounds":{"left":0.8572141,"top":0.27653632,"width":0.015791224,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp duration to clipboard","depth":13,"bounds":{"left":0.90392286,"top":0.2677574,"width":0.019946808,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from start","depth":12,"bounds":{"left":0.92918885,"top":0.27094972,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from failed","depth":12,"bounds":{"left":0.93982714,"top":0.27094972,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Cancel workflow","depth":12,"bounds":{"left":0.95046544,"top":0.27094972,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Fix workflow","depth":12,"bounds":{"left":0.96110374,"top":0.27094972,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"More Actions","depth":12,"bounds":{"left":0.97174203,"top":0.27094972,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Jobs","depth":14,"bounds":{"left":0.26080453,"top":0.32841182,"width":0.010804521,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job setup","depth":14,"bounds":{"left":0.28756648,"top":0.3256185,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"setup","depth":15,"bounds":{"left":0.30285904,"top":0.32841182,"width":0.012632979,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885222","depth":16,"bounds":{"left":0.3181516,"top":0.32841182,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"47s","depth":14,"bounds":{"left":0.9104056,"top":0.3256185,"width":0.008144947,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"47s","depth":15,"bounds":{"left":0.9104056,"top":0.32841182,"width":0.008144947,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app","depth":13,"bounds":{"left":0.12283909,"top":0.37789306,"width":0.00831117,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"58249","depth":12,"bounds":{"left":0.12283909,"top":0.3942538,"width":0.014295213,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"58249","depth":13,"bounds":{"left":0.12283909,"top":0.39505187,"width":0.014295213,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.","depth":13,"bounds":{"left":0.27759308,"top":0.37988827,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Status Running Running","depth":12,"bounds":{"left":0.28956118,"top":0.38148445,"width":0.03274601,"height":0.023144454},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":13,"bounds":{"left":0.30019948,"top":0.3858739,"width":0.018118352,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18m 29s","depth":13,"bounds":{"left":0.28956118,"top":0.4102155,"width":0.018450798,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remain","depth":13,"bounds":{"left":0.30801198,"top":0.4102155,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Info Outline","depth":12,"bounds":{"left":0.32496676,"top":0.4046289,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"build_accept_deploy","depth":12,"bounds":{"left":0.3444149,"top":0.38467678,"width":0.045545213,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build_accept_deploy","depth":13,"bounds":{"left":0.3444149,"top":0.3858739,"width":0.045545213,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":13,"bounds":{"left":0.5728058,"top":0.3782921,"width":0.11768617,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773-fix-automated-reports-user-pilot-tracking","depth":14,"bounds":{"left":0.5728058,"top":0.3790902,"width":0.11768617,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Open commit on version control site","depth":14,"bounds":{"left":0.5728058,"top":0.39704707,"width":0.12765957,"height":0.033519555},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"62861fa","depth":16,"bounds":{"left":0.5728058,"top":0.39864326,"width":0.018118352,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking","depth":16,"bounds":{"left":0.5728058,"top":0.39864326,"width":0.111369684,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Push","depth":13,"bounds":{"left":0.7190825,"top":0.3858739,"width":0.011303191,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commit pushed","depth":14,"bounds":{"left":0.73171544,"top":0.3858739,"width":0.034574468,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp to clipboard","depth":13,"bounds":{"left":0.85106385,"top":0.3782921,"width":0.027593086,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7m ago","depth":15,"bounds":{"left":0.85671544,"top":0.38747007,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy timestamp duration to clipboard","depth":13,"bounds":{"left":0.89544547,"top":0.3782921,"width":0.028424202,"height":0.031923383},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from start","depth":12,"bounds":{"left":0.92918885,"top":0.38148445,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Rerun workflow from failed","depth":12,"bounds":{"left":0.93982714,"top":0.38148445,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Cancel workflow","depth":12,"bounds":{"left":0.95046544,"top":0.38148445,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Fix workflow","depth":12,"bounds":{"left":0.96110374,"top":0.38148445,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"More Actions","depth":12,"bounds":{"left":0.97174203,"top":0.38148445,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Jobs","depth":14,"bounds":{"left":0.26080453,"top":0.45650437,"width":0.010804521,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job checkout-code","depth":14,"bounds":{"left":0.28756648,"top":0.45411015,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"checkout-code","depth":15,"bounds":{"left":0.30285904,"top":0.45650437,"width":0.03374335,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885213","depth":16,"bounds":{"left":0.33926198,"top":0.45650437,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 38s","depth":14,"bounds":{"left":0.9020944,"top":0.45411015,"width":0.016456118,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 38s","depth":15,"bounds":{"left":0.9020944,"top":0.45650437,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job build-frontend","depth":14,"bounds":{"left":0.28756648,"top":0.47964883,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build-frontend","depth":15,"bounds":{"left":0.30285904,"top":0.4820431,"width":0.032247342,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885216","depth":16,"bounds":{"left":0.33776596,"top":0.4820431,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 29s","depth":14,"bounds":{"left":0.90226066,"top":0.47964883,"width":0.016289894,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 29s","depth":15,"bounds":{"left":0.90226066,"top":0.4820431,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job test-frontend","depth":14,"bounds":{"left":0.28756648,"top":0.5051876,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"test-frontend","depth":15,"bounds":{"left":0.30285904,"top":0.50758183,"width":0.029753989,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885218","depth":16,"bounds":{"left":0.3352726,"top":0.50758183,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 43s","depth":14,"bounds":{"left":0.9020944,"top":0.5051876,"width":0.016456118,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 43s","depth":15,"bounds":{"left":0.9020944,"top":0.50758183,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job build-backend","depth":14,"bounds":{"left":0.28756648,"top":0.53072625,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"build-backend","depth":15,"bounds":{"left":0.30285904,"top":0.5331205,"width":0.032081116,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885214","depth":16,"bounds":{"left":0.33759972,"top":0.5331205,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1m 12s","depth":14,"bounds":{"left":0.9030917,"top":0.53072625,"width":0.015458777,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1m 12s","depth":15,"bounds":{"left":0.9030917,"top":0.5331205,"width":0.015458777,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job phpstan","depth":14,"bounds":{"left":0.28756648,"top":0.55626494,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"phpstan","depth":15,"bounds":{"left":0.30285904,"top":0.5586592,"width":0.018118352,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885219","depth":16,"bounds":{"left":0.32363698,"top":0.5586592,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2m 18s","depth":14,"bounds":{"left":0.90226066,"top":0.55626494,"width":0.016289894,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2m 18s","depth":15,"bounds":{"left":0.90226066,"top":0.5586592,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SUCCESS job setup","depth":14,"bounds":{"left":0.28756648,"top":0.5818037,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"setup","depth":15,"bounds":{"left":0.30285904,"top":0.58419794,"width":0.012632979,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885217","depth":16,"bounds":{"left":0.3181516,"top":0.58419794,"width":0.016123671,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"59s","depth":14,"bounds":{"left":0.91007316,"top":0.5818037,"width":0.008477394,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"59s","depth":15,"bounds":{"left":0.91007316,"top":0.58419794,"width":0.008477394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RUNNING job test","depth":14,"bounds":{"left":0.28756648,"top":0.60734236,"width":0.41805187,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"test","depth":15,"bounds":{"left":0.30285904,"top":0.6097366,"width":0.008643617,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"885220","depth":16,"bounds":{"left":0.31416222,"top":0.6097366,"width":0.017121011,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2m 22s","depth":14,"bounds":{"left":0.90176195,"top":0.60734236,"width":0.016788565,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7227207691354598865
|
-2592032537824234336
|
visual_change
|
accessibility
|
NULL
|
Unnamed Group
TypeError: League\Flysystem\Filesyst Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
Unnamed Group
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
Data Explorer
Data Explorer
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Close tab
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Go to home page
Auto theme
Open notifications
Open support menu
Open user menu
org avatar Current organization: jiminny
Home
Home
Pipelines
Pipelines
Projects
Projects
Deploys
Deploys
Insights
Insights
Runners
Runners
Org
Org
Plan
Plan
Chunk sidecars
Chunk sidecars
PREVIEW
Chunk
Chunk
Smarter Testing is now in beta
Smarter Testing is now in beta
Get faster feedback on your tests while maintaining full confidence
Dismiss
Get Started
Get Started
Dashboard All Pipelines
All Pipelines
Project Outline app
app
app
app
Overview
Overview
Settings
Settings
Deploys
Deploys
Lightning Manage triggers
Manage triggers
Trigger Pipeline
Pipelines All pipelines my-pipelines-filter
All pipelines
app Project Filter. Selected "app"
app
All branches Branch Filter. Selected "All branches"
All branches
Start Time Cutoff date Arrow Drop Down
Cutoff date
All statuses Arrow Drop Down
All
statuses
Filter Display options
Display options
Pipeline
Status
Workflow
Checkout source
Trigger event
Start
Duration
Actions
app
58250
58250
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
25m 1s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
13a63f7
JY-20606: add provider detection
Push
Commit pushed
Copy timestamp to clipboard
8s ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
RUNNING job checkout-code
checkout-code
885223
9s
9s
build-frontend
885224
test-frontend
885225
build-backend
885226
phpstan
885239
prepare_deploy_revision_subenv
885230
build_docker_backend_code_subenv
885231
build_docker_worker_code_subenv
885232
build_docker_worker_video_code_subenv
885233
db_migrations_subenv
885235
deploy_docker_backend_code_subenv
885237
deploy_docker_worker_code_subenv
885238
deploy_docker_worker_video_code_subenv
885236
deploy_frontend_assets_to_s3_subenv
885234
setup
885228
test
885229
test-backend-lint
885227
sonar_cloud
885240
SUCCESS workflow setup-workflow. Collapse the workflow jobs list.
Status Passed Success
Success
setup-workflow
setup-workflow
SETUP
JY-20606-desktop-app-recall
JY-20606-desktop-app-recall
Open commit on version control site
13a63f7
JY-20606: add provider detection
Push
Commit pushed
Copy timestamp to clipboard
1m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job setup
setup
885222
47s
47s
app
58249
58249
RUNNING workflow build_accept_deploy. Collapse the workflow jobs list.
Status Running Running
Running
18m 29s
remain
Info Outline
build_accept_deploy
build_accept_deploy
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20773-fix-automated-reports-user-pilot-tracking
Open commit on version control site
62861fa
Merge branch 'master' into JY-20773-fix-automated-reports-user-pilot-tracking
Push
Commit pushed
Copy timestamp to clipboard
7m ago
Copy timestamp duration to clipboard
Rerun workflow from start
Rerun workflow from failed
Cancel workflow
Fix workflow
More Actions
Jobs
SUCCESS job checkout-code
checkout-code
885213
1m 38s
1m 38s
SUCCESS job build-frontend
build-frontend
885216
1m 29s
1m 29s
SUCCESS job test-frontend
test-frontend
885218
1m 43s
1m 43s
SUCCESS job build-backend
build-backend
885214
1m 12s
1m 12s
SUCCESS job phpstan
phpstan
885219
2m 18s
2m 18s
SUCCESS job setup
setup
885217
59s
59s
RUNNING job test
test
885220
2m 22s...
|
25220
|
NULL
|
NULL
|
NULL
|
|
19750
|
848
|
0
|
2026-05-11T13:33:07.774958+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778506387774_m1.jpg...
|
Firefox
|
Pipelines - jiminny/app — Work
|
True
|
app.circleci.com/pipelines/github/jiminny/app
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Go to home page
Auto theme
Open notifications
Open support menu
Open user menu
org avatar Current organization: jiminny
Home
Home
Pipelines
Pipelines
Projects
Projects
Deploys
Deploys
Insights
Insights
Runners
Runners
Org
Org
Plan
Plan
Chunk sidecars...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.18715277,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.21006945,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.23333333,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.25659722,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.27986112,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Go to home page","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Auto theme","depth":9,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open notifications","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Open support menu","depth":9,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Open user menu","depth":9,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"org avatar Current organization: jiminny","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Home","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pipelines","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Projects","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Projects","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Deploys","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Deploys","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Runners","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Runners","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Org","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Org","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Plan","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Plan","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chunk sidecars","depth":11,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7225775295756577050
|
-2912418478999170430
|
idle
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Go to home page
Auto theme
Open notifications
Open support menu
Open user menu
org avatar Current organization: jiminny
Home
Home
Pipelines
Pipelines
Projects
Projects
Deploys
Deploys
Insights
Insights
Runners
Runners
Org
Org
Plan
Plan
Chunk sidecars...
|
19748
|
NULL
|
NULL
|
NULL
|
|
11615
|
520
|
33
|
2026-05-09T06:41:47.069306+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778308907069_m2.jpg...
|
Code
|
Visual Studio Code
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
EditSelectionViewlerminalWindowHelp( Pull requests EditSelectionViewlerminalWindowHelp( Pull requests • screenpipe/screenpipe • GitHubDNS / Nameservers I Hostingen@ Nginx Proxy Manages@ Screenpipe — Archive(*SQLite Web: archive.db( SQLite Web: db.salitescreenpipI) DXP4800PLCV Оптичен инте:V NO COIDER COENENConnected to remote.AFFiNE - All In One KndAll docs • AFFiNEPayments Loaaen(NirDiamant/GenAl Aaentel Add SwarmSeNew Tab@ Location Loggel} Finance Hub%Finance Hub— New TabFinance Hub^ trancactions totallY Filters|« search..dd /mm /yyyyAll Sources+ → SearchYou can cợng a rongg ton locạ ivClone Panositony0993G OIRA, VRASISS TORA Y5NC ‹ 40 libl O | Alexieva kids- in 4h 19 mA]I100% (7 Sat 9 May 9:41:47do PaymentsI Upload CSV€ RefreshAll Statusesdd /mm/yyyyAll lypes@* v Sign InAll TagsOpen ChatShow All CommandsOpen RecentOpen File or Folde:New Untitled Text FileAR*NIPl +- 08-191...
|
NULL
|
-7225591028655957399
|
NULL
|
visual_change
|
ocr
|
NULL
|
EditSelectionViewlerminalWindowHelp( Pull requests EditSelectionViewlerminalWindowHelp( Pull requests • screenpipe/screenpipe • GitHubDNS / Nameservers I Hostingen@ Nginx Proxy Manages@ Screenpipe — Archive(*SQLite Web: archive.db( SQLite Web: db.salitescreenpipI) DXP4800PLCV Оптичен инте:V NO COIDER COENENConnected to remote.AFFiNE - All In One KndAll docs • AFFiNEPayments Loaaen(NirDiamant/GenAl Aaentel Add SwarmSeNew Tab@ Location Loggel} Finance Hub%Finance Hub— New TabFinance Hub^ trancactions totallY Filters|« search..dd /mm /yyyyAll Sources+ → SearchYou can cợng a rongg ton locạ ivClone Panositony0993G OIRA, VRASISS TORA Y5NC ‹ 40 libl O | Alexieva kids- in 4h 19 mA]I100% (7 Sat 9 May 9:41:47do PaymentsI Upload CSV€ RefreshAll Statusesdd /mm/yyyyAll lypes@* v Sign InAll TagsOpen ChatShow All CommandsOpen RecentOpen File or Folde:New Untitled Text FileAR*NIPl +- 08-191...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
18270
|
790
|
40
|
2026-05-11T11:07:34.860056+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778497654860_m1.jpg...
|
Notion Calendar
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0(ab)Preparation for Refi... in 53 mDEV (docker)-zshDOCKER- ₴81DEV (docker)₴2APP (-zsh)H3worker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00:stoppedworker-nudges:worker-nudges_00:stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker:worker_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00:stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-es-update:worker-es-update_00: stoppedworker-emails:worker-emails_00: stoppedworker-audio:worker-audio_00: stoppedartisan-schedule:artisan-schedule_00:stoppedartisan-schedule:artisan-schedule_00:startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00: startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmDatajobs (portal=2)Done.Watch logs and run jiminny:debug observeRateLimit to inspect cachestate.root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart allINFOClearing cached bootstrap files.configcachecompiledeventsroutesviews84-zshX5screenpipe"100% <8• Mon 11 May 14:07:34181O ₴6-zsh87+DEV59.22ms DONE97.76ms DONE2.63ms DONE4.67ms DONE11.57ms DONE18.54ms DONE...
|
NULL
|
-7225465322759668190
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0(ab)Preparation for Refi... in 53 mDEV (docker)-zshDOCKER- ₴81DEV (docker)₴2APP (-zsh)H3worker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00:stoppedworker-nudges:worker-nudges_00:stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker:worker_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00:stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-es-update:worker-es-update_00: stoppedworker-emails:worker-emails_00: stoppedworker-audio:worker-audio_00: stoppedartisan-schedule:artisan-schedule_00:stoppedartisan-schedule:artisan-schedule_00:startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00: startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmDatajobs (portal=2)Done.Watch logs and run jiminny:debug observeRateLimit to inspect cachestate.root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart allINFOClearing cached bootstrap files.configcachecompiledeventsroutesviews84-zshX5screenpipe"100% <8• Mon 11 May 14:07:34181O ₴6-zsh87+DEV59.22ms DONE97.76ms DONE2.63ms DONE4.67ms DONE11.57ms DONE18.54ms DONE...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20841
|
904
|
7
|
2026-05-11T16:22:21.488856+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778516541488_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Collapse sidebar
Search
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
Hubspot BadRequest headers debugging
More options for Hubspot BadRequest headers debugging
Screenpipe sync script failing after recent migrations
More options for Screenpipe sync script failing after recent migrations
Monthly expense tracking
More options for Monthly expense tracking
Exporting transaction data from Notion to finance hub
More options for Exporting transaction data from Notion to finance hub
💬 How much have I spent for groc…
More options for 💬 How much have I spent for groc…
April 2026 spending by category
More options for April 2026 spending by category
Code diff review
More options for Code diff review
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Lukas Pro
Get apps and extensions
Screenpipe sync script failing after recent migrations, rename chat
Screenpipe sync script failing after recent migrations
More options for Screenpipe sync script failing after recent migrations
Open sidebar
Share chat
Claude finished the response
You said: after recent updated in screenpipe (find out what are these) I am unable to run script.
You said: after recent updated in screenpipe (find out what are these) I am unable to run script.
Pasted Text, pasted, 353 lines
#!/bin/bash # screenpipe_sync.sh # Syncs Screenpipe SQLite data to a NAS archive database (append-only, no deletions). # Also copies the day's video/frame data folder to the NAS. # # Usage: # ./screenpipe_sync.sh # syncs yesterday (default) # ./screenpipe_sync.sh 2026-04-15 # sync
PASTED
after recent updated in screenpipe (find out what are these) I am unable to run script. (pasted) "lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ ~/.screenpipe/screenpipe_sync.sh 2026-05-07
[2026-05-10 11:50:45] ========================================
[2026-05-10 11:50:45] Screenpipe sync starting for: 2026-05-07
[2026-05-10 11:50:45] ========================================
[+00m00s] ▶ Preflight checks
Source DB: OK (2.2G)
NAS mount: OK /Volumes/screenpipe
Archive DB: exists ( 10G)
Data dir: OK (266 files, 292M)
[+00m05s] ▶ Counting source rows for 2026-05-07
frames: 6262
elements: 623002
ui_events: 7412
ocr_text: 1670
meetings: 2
[+00m05s] ▶ Initialising tables, indexes, FTS
creating tables ✓ 0m00s
creating indexes ✓ 0m01s
creating FTS tables ✓ 0m00s
[+00m06s] ▶ Syncing data for 2026-05-07
video_chunks ✓ 0m01s
frames (6262 rows) ⠋ Parse error near line 3: table nas.frames has 24 columns but 30 values were supplied" There were some recent changes in migrations. Here are migrations form the begining of march (approx after I installed irt first time) 20260301000000 create elements table 2026-05-06 17:27:34 True 736637f38c6e0b5547f23c870ebbc3e87ef2d8d33b22ce73f7 ... 1302167
20260301100000 fts external content 2026-05-06 17:27:34 True 44ca0e5fc3b23c19aa09d7ac3fea48de604032d5feced2615c ... 2102875
20260301200000 drop ui monitoring 2026-05-06 17:27:34 True 9ab8a4d8c0d602b491ef1a6ff36076fd7b7c12c05848201682 ... 620375
20260306000000 delete empty transcriptions 2026-05-06 17:27:34 True 5f991a21d663157a2bce5cb9f0729f02181eef817aaef5a0b8 ... 166792
20260309000000 add cloud blob id 2026-05-06 17:27:34 True e1588e32884ec5660d11bbaa995d767fb2172bb9732ad22319 ... 1450542
20260310000000 create memories 2026-05-06 17:27:34 True 4fd07e878de1dd5b8d184e7bca9ee4e6b2480bbf39e5a68ff7 ... 1135416
20260311000000 drop unused tables 2026-05-06 17:27:34 True 3d9eb9d327a61c4055b31e22082cd045e00bd7a875cbdee86b ... 547625
20260312000000 consolidate search to frames full text 2026-05-06 17:27:34 True 5a7a31a359e9e93978d46ab4759fc8cd43898c0fd325d001b7 ... 3038250
20260312000001 drop dead fts tables 2026-05-06 17:27:34 True dd8264b96b4427f40b06ac60b813b77b6d055b24dd727212c5 ... 297250
20260312000002 drop accessibility tags 2026-05-06 17:27:34 True 672b2661f7e0fc8026f2eb6cc5d24935a15db4ed4982aeb973 ... 260167
20260315000000 add frame id to memories 2026-05-06 17:27:34 True f324ec7981134e647b6497126a2b6a7467e94d271d140d0d25 ... 642250
20260316000000 add elements activity summary index 2026-05-06 17:27:34 True 5b3f99a0d58fc73d62f240319d0718963364fdee1e3a7c4866 ... 265834
20260317000000 add elements automation props 2026-05-06 17:27:34 True 4bd132d263de143c7bb0dcf2e3b8074606c58c0f79e6091d13 ... 537750
20260318000000 add elements ref frame id 2026-05-06 17:27:34 True 33282b2c342e4743f096d1e3093146e243d97f392fe4df2cb5 ... 525250
20260319000000 add sync id indexes 2026-05-06 17:27:34 True 22c7a18c918cfcc458f05fdbfe2a0b2bb65a67ae9daeec6028 ... 407083
20260320000000 add note to meetings 2026-05-06 17:27:34 True cfa45b4c98e300c40cd36942839aa20528f47ae3e7b9c86751 ... 519625
20260324000000 drop ocr text delete trigger 2026-05-06 17:27:34 True 99f445308168fc88f993c43f8e884cc4dc7e41411c86b4d3e7 ... 182209
20260326000000 add session path to pipe executions 2026-05-06 17:27:34 True 5aa266dfcd7b741a18dd3ffb6b0ca3caf2e569959074cbc3ff ... 549583
20260411000000 add elements ref frame id index 2026-05-06 17:27:34 True 378589322920e74980ea48c6b44c916cd488e47a8f6172161e ... 232083
20260415000000 frames fts external content 2026-05-06 17:27:34 True 3fdac70da2fd44bca3c3717768b57c4cbee743451b73f13de9 ... 1259250
20260423000000 add speaker id indexes 2026-05-06 17:27:34 True 80e8e0cf144ee620c81b3796196ca22b8e34f9c152ca2ffe8f ... 398709
20260429000000 add evicted at to chunks 2026-05-06 17:27:34 True 180b1d44305df17620929a69ecce60fa3ce0148905adf19ff7 ... 963833
20260502000000 add elements on screen 2026-05-06 17:27:34 True 2cf51a077a75ef32e4e1d722092053ce919bdeee151d3d91fb ... 644500
20260504120000 add document path column 2026-05-06 17:27:34 True d84f7127e1647984eeb1f89e8dffd0918525f128e4608d323f ... 593042
20260506120000 add memories sync columns 2026-05-07 06:45:22 True 800beaaa228c7fbd88fe367dc5b357adb72a3a0635da5a30f4 ... 1777417
20260501000000 add redacted columns 2026-05-08 06:25:45 True 2a4cd6714a2322ccf8f2af6084af5a05e40f460c44c1aaf874 ... 795120667
20260503000000 add frames image redacted 2026-05-08 06:25:45 True . It worked up to the point where I attemtped to remove it and install again since the retention policy doesn't clear old data and it was to big. I am basically moving data to nas and query it form there. mac is for capture only or lets say one week query using claude code. THe main consumer and interface is on nas. How to make it work so new changes won't messup my setup. Seems like I removed the whole folder it was working find runnign with @letest even I may be wrong.
Show more
10 May
Retry
Edit
Copy
Claude responded: Looking at this carefully, the error tells the whole story:
Claude responded: Looking at this carefully, the error tells the whole story:
Pinpointing schema mismatches from recent PII redaction migrations
Pinpointing schema mismatches from recent PII redaction migrations
Looking at this carefully, the error tells the whole story:
Copy to clipboard
table nas.frames has 24 columns but 30 values were supplied
Your source DB got 6 new columns from recent migrations, but the NAS archive was created earlier with the old schema.
CREATE TABLE IF NOT EXISTS
is a no-op when the table exists, so the NAS schema is frozen at whatever it was when first created. Then
INSERT ... SELECT *
does positional mapping, which blows up the moment column counts diverge.
Let me verify the recent screenpipe changes and then fix this properly.
Diagnosed schema drift, architected resilient migration strategy
Diagnosed schema drift, architected resilient migration strategy
What's happening
What's happening
The error is schema drift, not a screenpipe bug. Your source DB grew 6 columns from these migrations (most landed late April / early May with the PII work):
20260429000000 add evicted at to chunks
→
video_chunks.evicted_at
20260502000000 add elements on screen
→
elements.on_screen
20260504120000 add document path column
→
frames.document_path
20260501000000 add redacted columns
→ redacted columns on frames/ocr_text/elements (this is the big one — accompanies the new
screenpipe/pii-redactor
model)
20260503000000 add frames image redacted
→
frames.image_redacted_path
20260506120000 add memories sync columns
→ memories table
Your NAS
archive.db
was created before these landed.
CREATE TABLE IF NOT EXISTS
is a no-op on existing tables, so the NAS schema froze. Then
INSERT INTO nas.frames SELECT * FROM main.frames
does
positional
mapping → 30 source values into a 24-column NAS table → parse error.
You weren't wrong about
npx screenpipe@latest
working: removing the folder gave you a fresh local DB with the new schema, and querying still works because everything is on one schema. The mismatch only surfaces when you try to merge old + new schemas via the sync.
The fix
The fix
Two changes make this future-proof:
ensure_columns
— before each insert, diff source vs NAS columns and run
ALTER TABLE ADD COLUMN
for any new ones. SQLite handles this as a metadata-only op, so it's instant even on your 10G archive.
Explicit column lists in INSERT
— never
SELECT *
again. Build the column list dynamically from
PRAGMA table_info(<table>)
and use
INSERT INTO t (cols) SELECT cols FROM ...
. Now positional drift is structurally impossible.
Bonus: dropped columns in future migrations will also be handled gracefully — the archive keeps the column with NULLs for new rows, no errors.
Let me write the updated script.
Created a file, ran a command, read a file
Created a file, ran a command, read a file
What changed in the script
What changed in the script
Three additions, everything else preserved:
Schema helpers
(
table_columns_with_types
,
table_columns
,
table_exists
,
ensure_columns
,
build_col_list
) — pure bash + sqlite3 PRAGMA, no extra deps.
New "Reconciling NAS schema with source" step
— runs after
CREATE TABLE IF NOT EXISTS
, before indexes/FTS/inserts. For each table in
SYNC_TABLES
, it diffs source columns against NAS and
ALTER TABLE ADD COLUMN
s the missing ones. Output looks like:
Copy to clipboard
schema: frames ✓ added 4: cloud_blob_id document_path image_redacted_path text_redacted
schema: elements ✓ added 2: on_screen text_redacted
schema: video_chunks ✓ added 1: evicted_at
Explicit column lists in every INSERT
—
FRAMES_COLS=$(build_col_list frames)
builds
"id","timestamp","app_name",...
from source
PRAGMA table_info
, and the inserts use
INSERT INTO nas.t (cols) SELECT cols FROM main.t
. No more positional mapping. Column names are double-quoted so any future hyphens or reserved-word columns won't break parsing.
What to expect on the next run
What to expect on the next run
First run after dropping it in: the "Reconciling NAS schema with source" step will print the missing columns it adds to your existing 10G archive (4 on
frames
, plus a few on
elements
/
video_chunks
/
ocr_text
). Each
ALTER TABLE ADD COLUMN
is a metadata-only op in SQLite — no table rewrite, no VACUUM needed, total cost should be sub-second even on the 10G file. Then 2026-05-07 will sync normally.
After that, when Screenpipe ships the next migration, your sync just picks up the new columns automatically. No more breakage.
One thing worth knowing
One thing worth knowing
If Screenpipe ever
drops
a column in source (rare but possible — they did
drop ui monitoring
and
drop dead fts tables
earlier), the NAS will still have it, and
build_col_list
(sourced from
main
) won't reference it. New rows will have NULL for the orphaned column, old rows keep their data. That's the right archival behavior — but if you ever want to actually drop those orphan columns from NAS, that's a manual
ALTER TABLE ... DROP COLUMN
which SQLite has supported since 3.35 (your macOS Tahoe ships 3.43+, and your NAS Docker SQLite is fine).
View Screenpipe sync
Screenpipe sync
SH
Open Screenpipe sync
Open in iTerm
Copy
Give positive feedback
Give negative feedback
Retry
You said: ok but given the changes introduced is there something I may actually use
You said: ok but given the changes introduced is there something I may actually use
ok but given the changes introduced is there something I may actually use
10 May
Retry
Edit
Copy
Claude responded: Good question.
Claude responded: Good question.
Cataloging PII redactor capabilities and security categories
Cataloging PII redactor capabilities and security categories
Good question. Let me look at what these new columns actually do and which ones map to your setup, since not all of them are equal value for you.
Evaluated four practical database columns for his specific workflow needs
Evaluated four practical database columns for his specific workflow needs
Yeah, three of them are genuinely useful for your setup. The rest are cloud-sync plumbing you can ignore.
frames.document_path — most useful for you
frames.document_path
— most useful for you
Captures the path of the document/file open at frame time. This is a meaningful upgrade over relying on
app_name
+
window_name
for IDE work.
Concrete uses given your stack:
Daily activity summary
: instead of "spent 2h in PhpStorm", you can group by
document_path
and get "spent 2h in
app/Http/Controllers/ReportsController.php
" — directly tied to the JY-20458 work.
Ticket correlation
: branch names contain ticket IDs in PhpStorm window titles, but file paths are cleaner. Query
WHERE document_path LIKE '%/reports/%'
to slice by feature area.
Replaces some of the
LIKE '%JY-%'
fishing
: your hyphenated-token issue with FTS5 mostly comes up in OCR text. For "what was I working on",
document_path
skips OCR entirely.
Worth adding an index on the NAS:
Copy to clipboard
sql
CREATE
INDEX
IF
NOT
EXISTS
idx_frames_document_path
ON
frames
(
document_path
)
WHERE
document_path
IS
NOT
NULL
;
elements.on_screen — directly useful for the meeting detector
elements.on_screen
— directly useful for the meeting detector
This is a boolean for whether the element was actually visible (vs. in the accessibility tree but scrolled off / occluded / behind a collapsed panel).
For your
meeting-detector
pipeline:
Zoom's accessibility tree exposes
everyone
on the call, including names in collapsed roster panels. Adding
WHERE on_screen = 1
cuts the participant detector down to people whose name tile was actually rendered — much closer to "who was visible during this segment".
For daily summaries: weights time-on-element by visibility, so reading vs. having-a-tab-open look different.
Cheap to add, big signal-to-noise win.
PII redaction columns (text_redacted etc.) — useful but with caveats
PII redaction columns (
text_redacted
etc.) — useful but with caveats
The model behind these is
screenpipe/pii-redactor
, fine-tuned for accessibility trees, OCR'd screen text, and computer-use traces. Detects 12 categories including a
secret
class that catches passwords, API keys, JWTs, DB connection strings, and PRIVATE-KEY markers — exactly the stuff that leaks into your captures during Laravel/Salesforce/HubSpot dev work.
For your setup, this maps well because:
The NAS archive is the consumer, queried by Claude Code agentically. Pointing those queries at
text_redacted
rather than
text
reduces the blast radius if a prompt or tool result ends up somewhere unexpected.
Your Mac captures Jiminny customer data on every call. Redacted text is safer for archival queries that don't need exact content (which is most of them).
Caveats worth knowing before flipping it on:
Probably opt-in.
Migrations only added the columns; redaction itself almost certainly needs a flag (worth checking
screenpipe record --help
for something like
--enable-pii-redaction
). Without enabling, the columns just stay NULL.
Over-redacts on short strings
: ~8% on accessibility/window titles, ~16% on long-form. Bulgarian names + Slovak surnames in your genealogy notes will likely trigger false positives. Keep the unredacted
text
column for cases where you need ground truth.
Model license is CC BY-NC 4.0
— non-commercial only, so not suitable for anything Jiminny-business related; fine for personal use.
Practical pattern: query
text_redacted
by default, fall back to
text
only when you explicitly need the raw value.
video_chunks.evicted_at — minor diagnostic value
video_chunks.evicted_at
— minor diagnostic value
When the local retention policy prunes a chunk, this stamps the time. Given your 7-day retention + nightly sync to NAS, you could add a preflight check that warns if a chunk got evicted before its frames were synced (would mean you have frame metadata pointing to deleted video). Realistically your nightly sync at 3am is well inside the 7-day window, so this is more "nice diagnostic" than "must-use".
Skip these
Skip these
frames.cloud_blob_id
,
memories sync columns
— for the optional E2EE cloud sync feature. You self-host, you don't need them. Stays NULL.
If you want, I can draft a small SQL migration for the NAS-side indexes (...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Collapse sidebar","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hubspot BadRequest headers debugging","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Hubspot BadRequest headers debugging","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync script failing after recent migrations","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync script failing after recent migrations","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Monthly expense tracking","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Monthly expense tracking","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Exporting transaction data from Notion to finance hub","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Exporting transaction data from Notion to finance hub","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 How much have I spent for groc…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 How much have I spent for groc…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"April 2026 spending by category","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for April 2026 spending by category","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code diff review","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Code diff review","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync script failing after recent migrations, rename chat","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Screenpipe sync script failing after recent migrations","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for Screenpipe sync script failing after recent migrations","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open sidebar","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: after recent updated in screenpipe (find out what are these) I am unable to run script.","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: after recent updated in screenpipe (find out what are these) I am unable to run script.","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Pasted Text, pasted, 353 lines","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"#!/bin/bash # screenpipe_sync.sh # Syncs Screenpipe SQLite data to a NAS archive database (append-only, no deletions). # Also copies the day's video/frame data folder to the NAS. # # Usage: # ./screenpipe_sync.sh # syncs yesterday (default) # ./screenpipe_sync.sh 2026-04-15 # sync","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PASTED","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"after recent updated in screenpipe (find out what are these) I am unable to run script. (pasted) \"lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ ~/.screenpipe/screenpipe_sync.sh 2026-05-07\n[2026-05-10 11:50:45] ========================================\n[2026-05-10 11:50:45] Screenpipe sync starting for: 2026-05-07\n[2026-05-10 11:50:45] ========================================\n[+00m00s] ▶ Preflight checks\n Source DB: OK (2.2G)\n NAS mount: OK /Volumes/screenpipe\n Archive DB: exists ( 10G)\n Data dir: OK (266 files, 292M)\n[+00m05s] ▶ Counting source rows for 2026-05-07\n frames: 6262\n elements: 623002\n ui_events: 7412\n ocr_text: 1670\n meetings: 2\n[+00m05s] ▶ Initialising tables, indexes, FTS\n creating tables ✓ 0m00s\n creating indexes ✓ 0m01s\n creating FTS tables ✓ 0m00s\n[+00m06s] ▶ Syncing data for 2026-05-07\n video_chunks ✓ 0m01s\n frames (6262 rows) ⠋ Parse error near line 3: table nas.frames has 24 columns but 30 values were supplied\" There were some recent changes in migrations. Here are migrations form the begining of march (approx after I installed irt first time) 20260301000000 create elements table 2026-05-06 17:27:34 True 736637f38c6e0b5547f23c870ebbc3e87ef2d8d33b22ce73f7 ... 1302167\n20260301100000 fts external content 2026-05-06 17:27:34 True 44ca0e5fc3b23c19aa09d7ac3fea48de604032d5feced2615c ... 2102875\n20260301200000 drop ui monitoring 2026-05-06 17:27:34 True 9ab8a4d8c0d602b491ef1a6ff36076fd7b7c12c05848201682 ... 620375\n20260306000000 delete empty transcriptions 2026-05-06 17:27:34 True 5f991a21d663157a2bce5cb9f0729f02181eef817aaef5a0b8 ... 166792\n20260309000000 add cloud blob id 2026-05-06 17:27:34 True e1588e32884ec5660d11bbaa995d767fb2172bb9732ad22319 ... 1450542\n20260310000000 create memories 2026-05-06 17:27:34 True 4fd07e878de1dd5b8d184e7bca9ee4e6b2480bbf39e5a68ff7 ... 1135416\n20260311000000 drop unused tables 2026-05-06 17:27:34 True 3d9eb9d327a61c4055b31e22082cd045e00bd7a875cbdee86b ... 547625\n20260312000000 consolidate search to frames full text 2026-05-06 17:27:34 True 5a7a31a359e9e93978d46ab4759fc8cd43898c0fd325d001b7 ... 3038250\n20260312000001 drop dead fts tables 2026-05-06 17:27:34 True dd8264b96b4427f40b06ac60b813b77b6d055b24dd727212c5 ... 297250\n20260312000002 drop accessibility tags 2026-05-06 17:27:34 True 672b2661f7e0fc8026f2eb6cc5d24935a15db4ed4982aeb973 ... 260167\n20260315000000 add frame id to memories 2026-05-06 17:27:34 True f324ec7981134e647b6497126a2b6a7467e94d271d140d0d25 ... 642250\n20260316000000 add elements activity summary index 2026-05-06 17:27:34 True 5b3f99a0d58fc73d62f240319d0718963364fdee1e3a7c4866 ... 265834\n20260317000000 add elements automation props 2026-05-06 17:27:34 True 4bd132d263de143c7bb0dcf2e3b8074606c58c0f79e6091d13 ... 537750\n20260318000000 add elements ref frame id 2026-05-06 17:27:34 True 33282b2c342e4743f096d1e3093146e243d97f392fe4df2cb5 ... 525250\n20260319000000 add sync id indexes 2026-05-06 17:27:34 True 22c7a18c918cfcc458f05fdbfe2a0b2bb65a67ae9daeec6028 ... 407083\n20260320000000 add note to meetings 2026-05-06 17:27:34 True cfa45b4c98e300c40cd36942839aa20528f47ae3e7b9c86751 ... 519625\n20260324000000 drop ocr text delete trigger 2026-05-06 17:27:34 True 99f445308168fc88f993c43f8e884cc4dc7e41411c86b4d3e7 ... 182209\n20260326000000 add session path to pipe executions 2026-05-06 17:27:34 True 5aa266dfcd7b741a18dd3ffb6b0ca3caf2e569959074cbc3ff ... 549583\n20260411000000 add elements ref frame id index 2026-05-06 17:27:34 True 378589322920e74980ea48c6b44c916cd488e47a8f6172161e ... 232083\n20260415000000 frames fts external content 2026-05-06 17:27:34 True 3fdac70da2fd44bca3c3717768b57c4cbee743451b73f13de9 ... 1259250\n20260423000000 add speaker id indexes 2026-05-06 17:27:34 True 80e8e0cf144ee620c81b3796196ca22b8e34f9c152ca2ffe8f ... 398709\n20260429000000 add evicted at to chunks 2026-05-06 17:27:34 True 180b1d44305df17620929a69ecce60fa3ce0148905adf19ff7 ... 963833\n20260502000000 add elements on screen 2026-05-06 17:27:34 True 2cf51a077a75ef32e4e1d722092053ce919bdeee151d3d91fb ... 644500\n20260504120000 add document path column 2026-05-06 17:27:34 True d84f7127e1647984eeb1f89e8dffd0918525f128e4608d323f ... 593042\n20260506120000 add memories sync columns 2026-05-07 06:45:22 True 800beaaa228c7fbd88fe367dc5b357adb72a3a0635da5a30f4 ... 1777417\n20260501000000 add redacted columns 2026-05-08 06:25:45 True 2a4cd6714a2322ccf8f2af6084af5a05e40f460c44c1aaf874 ... 795120667\n20260503000000 add frames image redacted 2026-05-08 06:25:45 True . It worked up to the point where I attemtped to remove it and install again since the retention policy doesn't clear old data and it was to big. I am basically moving data to nas and query it form there. mac is for capture only or lets say one week query using claude code. THe main consumer and interface is on nas. How to make it work so new changes won't messup my setup. Seems like I removed the whole folder it was working find runnign with @letest even I may be wrong.","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10 May","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Looking at this carefully, the error tells the whole story:","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Looking at this carefully, the error tells the whole story:","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Pinpointing schema mismatches from recent PII redaction migrations","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pinpointing schema mismatches from recent PII redaction migrations","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Looking at this carefully, the error tells the whole story:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"table nas.frames has 24 columns but 30 values were supplied","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your source DB got 6 new columns from recent migrations, but the NAS archive was created earlier with the old schema.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CREATE TABLE IF NOT EXISTS","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is a no-op when the table exists, so the NAS schema is frozen at whatever it was when first created. Then","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT ... SELECT *","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"does positional mapping, which blows up the moment column counts diverge.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Let me verify the recent screenpipe changes and then fix this properly.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Diagnosed schema drift, architected resilient migration strategy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Diagnosed schema drift, architected resilient migration strategy","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What's happening","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What's happening","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The error is schema drift, not a screenpipe bug. Your source DB grew 6 columns from these migrations (most landed late April / early May with the PII work):","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20260429000000 add evicted at to chunks","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"video_chunks.evicted_at","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20260502000000 add elements on screen","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"elements.on_screen","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20260504120000 add document path column","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames.document_path","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20260501000000 add redacted columns","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→ redacted columns on frames/ocr_text/elements (this is the big one — accompanies the new","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"screenpipe/pii-redactor","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"model)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20260503000000 add frames image redacted","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames.image_redacted_path","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20260506120000 add memories sync columns","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→ memories table","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your NAS","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"archive.db","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"was created before these landed.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CREATE TABLE IF NOT EXISTS","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is a no-op on existing tables, so the NAS schema froze. Then","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT INTO nas.frames SELECT * FROM main.frames","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"does","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"positional","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"mapping → 30 source values into a 24-column NAS table → parse error.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"You weren't wrong about","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"npx screenpipe@latest","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"working: removing the folder gave you a fresh local DB with the new schema, and querying still works because everything is on one schema. The mismatch only surfaces when you try to merge old + new schemas via the sync.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"The fix","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"The fix","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two changes make this future-proof:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ensure_columns","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— before each insert, diff source vs NAS columns and run","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ALTER TABLE ADD COLUMN","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"for any new ones. SQLite handles this as a metadata-only op, so it's instant even on your 10G archive.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explicit column lists in INSERT","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— never","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SELECT *","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"again. Build the column list dynamically from","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PRAGMA table_info(<table>)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and use","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT INTO t (cols) SELECT cols FROM ...","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". Now positional drift is structurally impossible.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bonus: dropped columns in future migrations will also be handled gracefully — the archive keeps the column with NULLs for new rows, no errors.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Let me write the updated script.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Created a file, ran a command, read a file","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Created a file, ran a command, read a file","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What changed in the script","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What changed in the script","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Three additions, everything else preserved:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Schema helpers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"table_columns_with_types","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"table_columns","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"table_exists","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ensure_columns","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"build_col_list","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") — pure bash + sqlite3 PRAGMA, no extra deps.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"New \"Reconciling NAS schema with source\" step","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— runs after","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CREATE TABLE IF NOT EXISTS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", before indexes/FTS/inserts. For each table in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SYNC_TABLES","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", it diffs source columns against NAS and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ALTER TABLE ADD COLUMN","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"s the missing ones. Output looks like:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"schema: frames ✓ added 4: cloud_blob_id document_path image_redacted_path text_redacted","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"schema: elements ✓ added 2: on_screen text_redacted","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"schema: video_chunks ✓ added 1: evicted_at","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explicit column lists in every INSERT","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FRAMES_COLS=$(build_col_list frames)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"builds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"id\",\"timestamp\",\"app_name\",...","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from source","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PRAGMA table_info","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and the inserts use","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT INTO nas.t (cols) SELECT cols FROM main.t","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". No more positional mapping. Column names are double-quoted so any future hyphens or reserved-word columns won't break parsing.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What to expect on the next run","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What to expect on the next run","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"First run after dropping it in: the \"Reconciling NAS schema with source\" step will print the missing columns it adds to your existing 10G archive (4 on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a few on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"elements","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"video_chunks","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ocr_text","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Each","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ALTER TABLE ADD COLUMN","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is a metadata-only op in SQLite — no table rewrite, no VACUUM needed, total cost should be sub-second even on the 10G file. Then 2026-05-07 will sync normally.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After that, when Screenpipe ships the next migration, your sync just picks up the new columns automatically. No more breakage.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"One thing worth knowing","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"One thing worth knowing","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If Screenpipe ever","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"drops","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"a column in source (rare but possible — they did","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"drop ui monitoring","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"drop dead fts tables","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"earlier), the NAS will still have it, and","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"build_col_list","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(sourced from","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"main","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") won't reference it. New rows will have NULL for the orphaned column, old rows keep their data. That's the right archival behavior — but if you ever want to actually drop those orphan columns from NAS, that's a manual","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ALTER TABLE ... DROP COLUMN","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"which SQLite has supported since 3.35 (your macOS Tahoe ships 3.43+, and your NAS Docker SQLite is fine).","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"View Screenpipe sync","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Screenpipe sync","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SH","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Open Screenpipe sync","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Open in iTerm","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: ok but given the changes introduced is there something I may actually use","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: ok but given the changes introduced is there something I may actually use","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ok but given the changes introduced is there something I may actually use","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10 May","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Good question.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Good question.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Cataloging PII redactor capabilities and security categories","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Cataloging PII redactor capabilities and security categories","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good question. Let me look at what these new columns actually do and which ones map to your setup, since not all of them are equal value for you.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Evaluated four practical database columns for his specific workflow needs","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Evaluated four practical database columns for his specific workflow needs","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yeah, three of them are genuinely useful for your setup. The rest are cloud-sync plumbing you can ignore.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"frames.document_path — most useful for you","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"frames.document_path","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— most useful for you","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Captures the path of the document/file open at frame time. This is a meaningful upgrade over relying on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app_name","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"window_name","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"for IDE work.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concrete uses given your stack:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily activity summary","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": instead of \"spent 2h in PhpStorm\", you can group by","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"document_path","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and get \"spent 2h in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app/Http/Controllers/ReportsController.php","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\" — directly tied to the JY-20458 work.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ticket correlation","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": branch names contain ticket IDs in PhpStorm window titles, but file paths are cleaner. Query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WHERE document_path LIKE '%/reports/%'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to slice by feature area.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Replaces some of the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LIKE '%JY-%'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"fishing","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": your hyphenated-token issue with FTS5 mostly comes up in OCR text. For \"what was I working on\",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"document_path","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"skips OCR entirely.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Worth adding an index on the NAS:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"sql","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CREATE","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INDEX","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IF","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"NOT","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXISTS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"idx_frames_document_path","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ON","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"document_path","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WHERE","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"document_path","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"NOT","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"NULL","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"elements.on_screen — directly useful for the meeting detector","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"elements.on_screen","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— directly useful for the meeting detector","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"This is a boolean for whether the element was actually visible (vs. in the accessibility tree but scrolled off / occluded / behind a collapsed panel).","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For your","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"meeting-detector","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"pipeline:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zoom's accessibility tree exposes","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"everyone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on the call, including names in collapsed roster panels. Adding","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WHERE on_screen = 1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cuts the participant detector down to people whose name tile was actually rendered — much closer to \"who was visible during this segment\".","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For daily summaries: weights time-on-element by visibility, so reading vs. having-a-tab-open look different.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cheap to add, big signal-to-noise win.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"PII redaction columns (text_redacted etc.) — useful but with caveats","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"PII redaction columns (","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"text_redacted","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"etc.) — useful but with caveats","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The model behind these is","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"screenpipe/pii-redactor","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", fine-tuned for accessibility trees, OCR'd screen text, and computer-use traces. Detects 12 categories including a","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secret","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"class that catches passwords, API keys, JWTs, DB connection strings, and PRIVATE-KEY markers — exactly the stuff that leaks into your captures during Laravel/Salesforce/HubSpot dev work.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For your setup, this maps well because:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The NAS archive is the consumer, queried by Claude Code agentically. Pointing those queries at","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"text_redacted","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"rather than","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"text","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"reduces the blast radius if a prompt or tool result ends up somewhere unexpected.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your Mac captures Jiminny customer data on every call. Redacted text is safer for archival queries that don't need exact content (which is most of them).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Caveats worth knowing before flipping it on:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Probably opt-in.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migrations only added the columns; redaction itself almost certainly needs a flag (worth checking","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"screenpipe record --help","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"for something like","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"--enable-pii-redaction","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Without enabling, the columns just stay NULL.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Over-redacts on short strings","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": ~8% on accessibility/window titles, ~16% on long-form. Bulgarian names + Slovak surnames in your genealogy notes will likely trigger false positives. Keep the unredacted","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"text","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"column for cases where you need ground truth.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model license is CC BY-NC 4.0","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— non-commercial only, so not suitable for anything Jiminny-business related; fine for personal use.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Practical pattern: query","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"text_redacted","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"by default, fall back to","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"text","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"only when you explicitly need the raw value.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"video_chunks.evicted_at — minor diagnostic value","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"video_chunks.evicted_at","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— minor diagnostic value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"When the local retention policy prunes a chunk, this stamps the time. Given your 7-day retention + nightly sync to NAS, you could add a preflight check that warns if a chunk got evicted before its frames were synced (would mean you have frame metadata pointing to deleted video). Realistically your nightly sync at 3am is well inside the 7-day window, so this is more \"nice diagnostic\" than \"must-use\".","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Skip these","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Skip these","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames.cloud_blob_id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"memories sync columns","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— for the optional E2EE cloud sync feature. You self-host, you don't need them. Stays NULL.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If you want, I can draft a small SQL migration for the NAS-side indexes (","depth":25,"on_screen":false,"role_description":"text"}]...
|
-7224861051431545664
|
2751663336271348754
|
idle
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Collapse sidebar
Search
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
Hubspot BadRequest headers debugging
More options for Hubspot BadRequest headers debugging
Screenpipe sync script failing after recent migrations
More options for Screenpipe sync script failing after recent migrations
Monthly expense tracking
More options for Monthly expense tracking
Exporting transaction data from Notion to finance hub
More options for Exporting transaction data from Notion to finance hub
💬 How much have I spent for groc…
More options for 💬 How much have I spent for groc…
April 2026 spending by category
More options for April 2026 spending by category
Code diff review
More options for Code diff review
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Lukas Pro
Get apps and extensions
Screenpipe sync script failing after recent migrations, rename chat
Screenpipe sync script failing after recent migrations
More options for Screenpipe sync script failing after recent migrations
Open sidebar
Share chat
Claude finished the response
You said: after recent updated in screenpipe (find out what are these) I am unable to run script.
You said: after recent updated in screenpipe (find out what are these) I am unable to run script.
Pasted Text, pasted, 353 lines
#!/bin/bash # screenpipe_sync.sh # Syncs Screenpipe SQLite data to a NAS archive database (append-only, no deletions). # Also copies the day's video/frame data folder to the NAS. # # Usage: # ./screenpipe_sync.sh # syncs yesterday (default) # ./screenpipe_sync.sh 2026-04-15 # sync
PASTED
after recent updated in screenpipe (find out what are these) I am unable to run script. (pasted) "lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe $ ~/.screenpipe/screenpipe_sync.sh 2026-05-07
[2026-05-10 11:50:45] ========================================
[2026-05-10 11:50:45] Screenpipe sync starting for: 2026-05-07
[2026-05-10 11:50:45] ========================================
[+00m00s] ▶ Preflight checks
Source DB: OK (2.2G)
NAS mount: OK /Volumes/screenpipe
Archive DB: exists ( 10G)
Data dir: OK (266 files, 292M)
[+00m05s] ▶ Counting source rows for 2026-05-07
frames: 6262
elements: 623002
ui_events: 7412
ocr_text: 1670
meetings: 2
[+00m05s] ▶ Initialising tables, indexes, FTS
creating tables ✓ 0m00s
creating indexes ✓ 0m01s
creating FTS tables ✓ 0m00s
[+00m06s] ▶ Syncing data for 2026-05-07
video_chunks ✓ 0m01s
frames (6262 rows) ⠋ Parse error near line 3: table nas.frames has 24 columns but 30 values were supplied" There were some recent changes in migrations. Here are migrations form the begining of march (approx after I installed irt first time) 20260301000000 create elements table 2026-05-06 17:27:34 True 736637f38c6e0b5547f23c870ebbc3e87ef2d8d33b22ce73f7 ... 1302167
20260301100000 fts external content 2026-05-06 17:27:34 True 44ca0e5fc3b23c19aa09d7ac3fea48de604032d5feced2615c ... 2102875
20260301200000 drop ui monitoring 2026-05-06 17:27:34 True 9ab8a4d8c0d602b491ef1a6ff36076fd7b7c12c05848201682 ... 620375
20260306000000 delete empty transcriptions 2026-05-06 17:27:34 True 5f991a21d663157a2bce5cb9f0729f02181eef817aaef5a0b8 ... 166792
20260309000000 add cloud blob id 2026-05-06 17:27:34 True e1588e32884ec5660d11bbaa995d767fb2172bb9732ad22319 ... 1450542
20260310000000 create memories 2026-05-06 17:27:34 True 4fd07e878de1dd5b8d184e7bca9ee4e6b2480bbf39e5a68ff7 ... 1135416
20260311000000 drop unused tables 2026-05-06 17:27:34 True 3d9eb9d327a61c4055b31e22082cd045e00bd7a875cbdee86b ... 547625
20260312000000 consolidate search to frames full text 2026-05-06 17:27:34 True 5a7a31a359e9e93978d46ab4759fc8cd43898c0fd325d001b7 ... 3038250
20260312000001 drop dead fts tables 2026-05-06 17:27:34 True dd8264b96b4427f40b06ac60b813b77b6d055b24dd727212c5 ... 297250
20260312000002 drop accessibility tags 2026-05-06 17:27:34 True 672b2661f7e0fc8026f2eb6cc5d24935a15db4ed4982aeb973 ... 260167
20260315000000 add frame id to memories 2026-05-06 17:27:34 True f324ec7981134e647b6497126a2b6a7467e94d271d140d0d25 ... 642250
20260316000000 add elements activity summary index 2026-05-06 17:27:34 True 5b3f99a0d58fc73d62f240319d0718963364fdee1e3a7c4866 ... 265834
20260317000000 add elements automation props 2026-05-06 17:27:34 True 4bd132d263de143c7bb0dcf2e3b8074606c58c0f79e6091d13 ... 537750
20260318000000 add elements ref frame id 2026-05-06 17:27:34 True 33282b2c342e4743f096d1e3093146e243d97f392fe4df2cb5 ... 525250
20260319000000 add sync id indexes 2026-05-06 17:27:34 True 22c7a18c918cfcc458f05fdbfe2a0b2bb65a67ae9daeec6028 ... 407083
20260320000000 add note to meetings 2026-05-06 17:27:34 True cfa45b4c98e300c40cd36942839aa20528f47ae3e7b9c86751 ... 519625
20260324000000 drop ocr text delete trigger 2026-05-06 17:27:34 True 99f445308168fc88f993c43f8e884cc4dc7e41411c86b4d3e7 ... 182209
20260326000000 add session path to pipe executions 2026-05-06 17:27:34 True 5aa266dfcd7b741a18dd3ffb6b0ca3caf2e569959074cbc3ff ... 549583
20260411000000 add elements ref frame id index 2026-05-06 17:27:34 True 378589322920e74980ea48c6b44c916cd488e47a8f6172161e ... 232083
20260415000000 frames fts external content 2026-05-06 17:27:34 True 3fdac70da2fd44bca3c3717768b57c4cbee743451b73f13de9 ... 1259250
20260423000000 add speaker id indexes 2026-05-06 17:27:34 True 80e8e0cf144ee620c81b3796196ca22b8e34f9c152ca2ffe8f ... 398709
20260429000000 add evicted at to chunks 2026-05-06 17:27:34 True 180b1d44305df17620929a69ecce60fa3ce0148905adf19ff7 ... 963833
20260502000000 add elements on screen 2026-05-06 17:27:34 True 2cf51a077a75ef32e4e1d722092053ce919bdeee151d3d91fb ... 644500
20260504120000 add document path column 2026-05-06 17:27:34 True d84f7127e1647984eeb1f89e8dffd0918525f128e4608d323f ... 593042
20260506120000 add memories sync columns 2026-05-07 06:45:22 True 800beaaa228c7fbd88fe367dc5b357adb72a3a0635da5a30f4 ... 1777417
20260501000000 add redacted columns 2026-05-08 06:25:45 True 2a4cd6714a2322ccf8f2af6084af5a05e40f460c44c1aaf874 ... 795120667
20260503000000 add frames image redacted 2026-05-08 06:25:45 True . It worked up to the point where I attemtped to remove it and install again since the retention policy doesn't clear old data and it was to big. I am basically moving data to nas and query it form there. mac is for capture only or lets say one week query using claude code. THe main consumer and interface is on nas. How to make it work so new changes won't messup my setup. Seems like I removed the whole folder it was working find runnign with @letest even I may be wrong.
Show more
10 May
Retry
Edit
Copy
Claude responded: Looking at this carefully, the error tells the whole story:
Claude responded: Looking at this carefully, the error tells the whole story:
Pinpointing schema mismatches from recent PII redaction migrations
Pinpointing schema mismatches from recent PII redaction migrations
Looking at this carefully, the error tells the whole story:
Copy to clipboard
table nas.frames has 24 columns but 30 values were supplied
Your source DB got 6 new columns from recent migrations, but the NAS archive was created earlier with the old schema.
CREATE TABLE IF NOT EXISTS
is a no-op when the table exists, so the NAS schema is frozen at whatever it was when first created. Then
INSERT ... SELECT *
does positional mapping, which blows up the moment column counts diverge.
Let me verify the recent screenpipe changes and then fix this properly.
Diagnosed schema drift, architected resilient migration strategy
Diagnosed schema drift, architected resilient migration strategy
What's happening
What's happening
The error is schema drift, not a screenpipe bug. Your source DB grew 6 columns from these migrations (most landed late April / early May with the PII work):
20260429000000 add evicted at to chunks
→
video_chunks.evicted_at
20260502000000 add elements on screen
→
elements.on_screen
20260504120000 add document path column
→
frames.document_path
20260501000000 add redacted columns
→ redacted columns on frames/ocr_text/elements (this is the big one — accompanies the new
screenpipe/pii-redactor
model)
20260503000000 add frames image redacted
→
frames.image_redacted_path
20260506120000 add memories sync columns
→ memories table
Your NAS
archive.db
was created before these landed.
CREATE TABLE IF NOT EXISTS
is a no-op on existing tables, so the NAS schema froze. Then
INSERT INTO nas.frames SELECT * FROM main.frames
does
positional
mapping → 30 source values into a 24-column NAS table → parse error.
You weren't wrong about
npx screenpipe@latest
working: removing the folder gave you a fresh local DB with the new schema, and querying still works because everything is on one schema. The mismatch only surfaces when you try to merge old + new schemas via the sync.
The fix
The fix
Two changes make this future-proof:
ensure_columns
— before each insert, diff source vs NAS columns and run
ALTER TABLE ADD COLUMN
for any new ones. SQLite handles this as a metadata-only op, so it's instant even on your 10G archive.
Explicit column lists in INSERT
— never
SELECT *
again. Build the column list dynamically from
PRAGMA table_info(<table>)
and use
INSERT INTO t (cols) SELECT cols FROM ...
. Now positional drift is structurally impossible.
Bonus: dropped columns in future migrations will also be handled gracefully — the archive keeps the column with NULLs for new rows, no errors.
Let me write the updated script.
Created a file, ran a command, read a file
Created a file, ran a command, read a file
What changed in the script
What changed in the script
Three additions, everything else preserved:
Schema helpers
(
table_columns_with_types
,
table_columns
,
table_exists
,
ensure_columns
,
build_col_list
) — pure bash + sqlite3 PRAGMA, no extra deps.
New "Reconciling NAS schema with source" step
— runs after
CREATE TABLE IF NOT EXISTS
, before indexes/FTS/inserts. For each table in
SYNC_TABLES
, it diffs source columns against NAS and
ALTER TABLE ADD COLUMN
s the missing ones. Output looks like:
Copy to clipboard
schema: frames ✓ added 4: cloud_blob_id document_path image_redacted_path text_redacted
schema: elements ✓ added 2: on_screen text_redacted
schema: video_chunks ✓ added 1: evicted_at
Explicit column lists in every INSERT
—
FRAMES_COLS=$(build_col_list frames)
builds
"id","timestamp","app_name",...
from source
PRAGMA table_info
, and the inserts use
INSERT INTO nas.t (cols) SELECT cols FROM main.t
. No more positional mapping. Column names are double-quoted so any future hyphens or reserved-word columns won't break parsing.
What to expect on the next run
What to expect on the next run
First run after dropping it in: the "Reconciling NAS schema with source" step will print the missing columns it adds to your existing 10G archive (4 on
frames
, plus a few on
elements
/
video_chunks
/
ocr_text
). Each
ALTER TABLE ADD COLUMN
is a metadata-only op in SQLite — no table rewrite, no VACUUM needed, total cost should be sub-second even on the 10G file. Then 2026-05-07 will sync normally.
After that, when Screenpipe ships the next migration, your sync just picks up the new columns automatically. No more breakage.
One thing worth knowing
One thing worth knowing
If Screenpipe ever
drops
a column in source (rare but possible — they did
drop ui monitoring
and
drop dead fts tables
earlier), the NAS will still have it, and
build_col_list
(sourced from
main
) won't reference it. New rows will have NULL for the orphaned column, old rows keep their data. That's the right archival behavior — but if you ever want to actually drop those orphan columns from NAS, that's a manual
ALTER TABLE ... DROP COLUMN
which SQLite has supported since 3.35 (your macOS Tahoe ships 3.43+, and your NAS Docker SQLite is fine).
View Screenpipe sync
Screenpipe sync
SH
Open Screenpipe sync
Open in iTerm
Copy
Give positive feedback
Give negative feedback
Retry
You said: ok but given the changes introduced is there something I may actually use
You said: ok but given the changes introduced is there something I may actually use
ok but given the changes introduced is there something I may actually use
10 May
Retry
Edit
Copy
Claude responded: Good question.
Claude responded: Good question.
Cataloging PII redactor capabilities and security categories
Cataloging PII redactor capabilities and security categories
Good question. Let me look at what these new columns actually do and which ones map to your setup, since not all of them are equal value for you.
Evaluated four practical database columns for his specific workflow needs
Evaluated four practical database columns for his specific workflow needs
Yeah, three of them are genuinely useful for your setup. The rest are cloud-sync plumbing you can ignore.
frames.document_path — most useful for you
frames.document_path
— most useful for you
Captures the path of the document/file open at frame time. This is a meaningful upgrade over relying on
app_name
+
window_name
for IDE work.
Concrete uses given your stack:
Daily activity summary
: instead of "spent 2h in PhpStorm", you can group by
document_path
and get "spent 2h in
app/Http/Controllers/ReportsController.php
" — directly tied to the JY-20458 work.
Ticket correlation
: branch names contain ticket IDs in PhpStorm window titles, but file paths are cleaner. Query
WHERE document_path LIKE '%/reports/%'
to slice by feature area.
Replaces some of the
LIKE '%JY-%'
fishing
: your hyphenated-token issue with FTS5 mostly comes up in OCR text. For "what was I working on",
document_path
skips OCR entirely.
Worth adding an index on the NAS:
Copy to clipboard
sql
CREATE
INDEX
IF
NOT
EXISTS
idx_frames_document_path
ON
frames
(
document_path
)
WHERE
document_path
IS
NOT
NULL
;
elements.on_screen — directly useful for the meeting detector
elements.on_screen
— directly useful for the meeting detector
This is a boolean for whether the element was actually visible (vs. in the accessibility tree but scrolled off / occluded / behind a collapsed panel).
For your
meeting-detector
pipeline:
Zoom's accessibility tree exposes
everyone
on the call, including names in collapsed roster panels. Adding
WHERE on_screen = 1
cuts the participant detector down to people whose name tile was actually rendered — much closer to "who was visible during this segment".
For daily summaries: weights time-on-element by visibility, so reading vs. having-a-tab-open look different.
Cheap to add, big signal-to-noise win.
PII redaction columns (text_redacted etc.) — useful but with caveats
PII redaction columns (
text_redacted
etc.) — useful but with caveats
The model behind these is
screenpipe/pii-redactor
, fine-tuned for accessibility trees, OCR'd screen text, and computer-use traces. Detects 12 categories including a
secret
class that catches passwords, API keys, JWTs, DB connection strings, and PRIVATE-KEY markers — exactly the stuff that leaks into your captures during Laravel/Salesforce/HubSpot dev work.
For your setup, this maps well because:
The NAS archive is the consumer, queried by Claude Code agentically. Pointing those queries at
text_redacted
rather than
text
reduces the blast radius if a prompt or tool result ends up somewhere unexpected.
Your Mac captures Jiminny customer data on every call. Redacted text is safer for archival queries that don't need exact content (which is most of them).
Caveats worth knowing before flipping it on:
Probably opt-in.
Migrations only added the columns; redaction itself almost certainly needs a flag (worth checking
screenpipe record --help
for something like
--enable-pii-redaction
). Without enabling, the columns just stay NULL.
Over-redacts on short strings
: ~8% on accessibility/window titles, ~16% on long-form. Bulgarian names + Slovak surnames in your genealogy notes will likely trigger false positives. Keep the unredacted
text
column for cases where you need ground truth.
Model license is CC BY-NC 4.0
— non-commercial only, so not suitable for anything Jiminny-business related; fine for personal use.
Practical pattern: query
text_redacted
by default, fall back to
text
only when you explicitly need the raw value.
video_chunks.evicted_at — minor diagnostic value
video_chunks.evicted_at
— minor diagnostic value
When the local retention policy prunes a chunk, this stamps the time. Given your 7-day retention + nightly sync to NAS, you could add a preflight check that warns if a chunk got evicted before its frames were synced (would mean you have frame metadata pointing to deleted video). Realistically your nightly sync at 3am is well inside the 7-day window, so this is more "nice diagnostic" than "must-use".
Skip these
Skip these
frames.cloud_blob_id
,
memories sync columns
— for the optional E2EE cloud sync feature. You self-host, you don't need them. Stays NULL.
If you want, I can draft a small SQL migration for the NAS-side indexes (...
|
20836
|
NULL
|
NULL
|
NULL
|
|
3576
|
132
|
18
|
2026-05-07T12:27:45.514202+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778156865514_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmancaltVIewWindowHubSpot rate limit implement PostmancaltVIewWindowHubSpot rate limit implementation strategymuluple Newman processes run concurrenty.Option 4: Postman Performance Testing newer feature)Ir your rostman version has it, collection Kunner has a Pertormance tad nowalonesiee runcuonal. luruns une conecuon wu conncuradie vntuar usersinparallel — set 20 virtual users, 30-second duration, and you'll saturate the burstwindow without any external tooling, Path is Run collection - Performance →contoureYou probably don t actually need to hit itWorth pausing here: the goal or this exercise is to veriy your limiter handles 429Sconecuy, nent. rou can do unat wiun a mock witnout durie real quora// In your test, take a 429 responseSmockResnonse = new Guzz lehttn Psrz Resnonse!429'Retrv-Afterl => 'g'.json encodeL'status' => 'error','message' => 'You have reached your secondly limit.'.'errorType' => 'RATE LIMIT'.oolicvName' =>"SECONDLY,'correlationid' => 'test-123',That gives you the exact shape HubSpot returns, and you can unit-test:• Parsing policvName correctlv• Routing to the right queue's backoff• Honoring Retrv-After• Not double-counting the failed call against the bucketPostman's valute is regllv one-ofl eynloration ("does this header exist? what does al429 body look like?"). For wiring up the limiter, mocked tests are faster and don'tKeep going in Claude CodeSwitch to Claude Code and let Claude work directiv in vour.Write a message…Opus 4. Adaptive*x Hubspot v• SearchYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaborationGET nexto • POST searc • POST Read •Run orderRun Sequence• geT Read CopyGET readGET Get Er•GET Read CoDeselect AllSelect All ResetIteration40"Lukas sterka 121• In 2n 3mNo environmentvCOLLECTIONS> Associations V4• CMS - URL Redirects APl Collection• Companies• COMPARE› Contactsv CRM Obiectsv crm/v3/objects/{object Type}>u batchD (object ld)› associations<to Obiect TypeGET Read5g. An error occurredca. successful onerationPaTCH Uindate>GET ListPOST Createy Post Filter, Sort, and Search CRM Obiectsca. succeccful onerationeg. An error occurred› CRM Owners• CRM Pioelines• Dealsv EngagementsIM OID ENGAGEMENTSGet list meetingsPoST soarch modified comnaniecPosT search tasksGET road call>PoST search callsGeT list callsPOST meetings scheduledGET get meetingPOST aet link to task> POSt Create Contact with Association> Hubspotv Iteration run HSGET Read Copyca. An error occurred.CaMiDANMeNre) spfcs>FLOWS§ Connect Git E Console TerrIterationD RunnerFunctionalPerformancechoose how to run vour pertormance test• In the app• Via the CLIContigure CLi command to run on your build pipelineSet up your performance testLoad profile ©Virtual users ©FixedI:120 Mule20 virtual users run for 10 minutes, each executina all requests sequentiallv.Data tile GSelect file>Pass test if...©100% L2Inu / May 10.21.40UparadeVAIIAll variablesE EnvironmentNo environment selected, seled enulommeaGlobalstokenCKPur5PqMxIZ@INOMi8kOfbaseUrlhttps://api.hubapi.comCeweToKenlCLLm5NnQMxIRQINQMI8kQ.• Local VaultStore your APl secrets locally in vault. Set up vaultGlobals Vault Tools?00O...
|
NULL
|
-7224504273120541902
|
NULL
|
click
|
ocr
|
NULL
|
PostmancaltVIewWindowHubSpot rate limit implement PostmancaltVIewWindowHubSpot rate limit implementation strategymuluple Newman processes run concurrenty.Option 4: Postman Performance Testing newer feature)Ir your rostman version has it, collection Kunner has a Pertormance tad nowalonesiee runcuonal. luruns une conecuon wu conncuradie vntuar usersinparallel — set 20 virtual users, 30-second duration, and you'll saturate the burstwindow without any external tooling, Path is Run collection - Performance →contoureYou probably don t actually need to hit itWorth pausing here: the goal or this exercise is to veriy your limiter handles 429Sconecuy, nent. rou can do unat wiun a mock witnout durie real quora// In your test, take a 429 responseSmockResnonse = new Guzz lehttn Psrz Resnonse!429'Retrv-Afterl => 'g'.json encodeL'status' => 'error','message' => 'You have reached your secondly limit.'.'errorType' => 'RATE LIMIT'.oolicvName' =>"SECONDLY,'correlationid' => 'test-123',That gives you the exact shape HubSpot returns, and you can unit-test:• Parsing policvName correctlv• Routing to the right queue's backoff• Honoring Retrv-After• Not double-counting the failed call against the bucketPostman's valute is regllv one-ofl eynloration ("does this header exist? what does al429 body look like?"). For wiring up the limiter, mocked tests are faster and don'tKeep going in Claude CodeSwitch to Claude Code and let Claude work directiv in vour.Write a message…Opus 4. Adaptive*x Hubspot v• SearchYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaborationGET nexto • POST searc • POST Read •Run orderRun Sequence• geT Read CopyGET readGET Get Er•GET Read CoDeselect AllSelect All ResetIteration40"Lukas sterka 121• In 2n 3mNo environmentvCOLLECTIONS> Associations V4• CMS - URL Redirects APl Collection• Companies• COMPARE› Contactsv CRM Obiectsv crm/v3/objects/{object Type}>u batchD (object ld)› associations<to Obiect TypeGET Read5g. An error occurredca. successful onerationPaTCH Uindate>GET ListPOST Createy Post Filter, Sort, and Search CRM Obiectsca. succeccful onerationeg. An error occurred› CRM Owners• CRM Pioelines• Dealsv EngagementsIM OID ENGAGEMENTSGet list meetingsPoST soarch modified comnaniecPosT search tasksGET road call>PoST search callsGeT list callsPOST meetings scheduledGET get meetingPOST aet link to task> POSt Create Contact with Association> Hubspotv Iteration run HSGET Read Copyca. An error occurred.CaMiDANMeNre) spfcs>FLOWS§ Connect Git E Console TerrIterationD RunnerFunctionalPerformancechoose how to run vour pertormance test• In the app• Via the CLIContigure CLi command to run on your build pipelineSet up your performance testLoad profile ©Virtual users ©FixedI:120 Mule20 virtual users run for 10 minutes, each executina all requests sequentiallv.Data tile GSelect file>Pass test if...©100% L2Inu / May 10.21.40UparadeVAIIAll variablesE EnvironmentNo environment selected, seled enulommeaGlobalstokenCKPur5PqMxIZ@INOMi8kOfbaseUrlhttps://api.hubapi.comCeweToKenlCLLm5NnQMxIRQINQMI8kQ.• Local VaultStore your APl secrets locally in vault. Set up vaultGlobals Vault Tools?00O...
|
3574
|
NULL
|
NULL
|
NULL
|
|
16318
|
732
|
1
|
2026-05-11T08:40:26.513407+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778488826513_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16312
|
NULL
|
NULL
|
NULL
|
|
16355
|
734
|
3
|
2026-05-11T08:45:49.335768+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778489149335_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16352
|
NULL
|
NULL
|
NULL
|
|
16356
|
735
|
5
|
2026-05-11T08:45:51.957550+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778489151957_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42885637,"top":0.09736632,"width":0.5711436,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42885637,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42885637,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42885637,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42885637,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42885637,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42885637,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42885637,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42885637,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42885637,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42885637,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42885637,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42885637,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42885637,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42885637,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42885637,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42885637,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42885637,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42885637,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42885637,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42885637,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42885637,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42885637,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42885637,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42885637,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16354
|
NULL
|
NULL
|
NULL
|
|
16362
|
734
|
6
|
2026-05-11T08:46:21.199676+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778489181199_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16363
|
735
|
9
|
2026-05-11T08:46:21.199698+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778489181199_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42885637,"top":0.09736632,"width":0.5711436,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42885637,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42885637,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42885637,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42885637,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42885637,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42885637,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42885637,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42885637,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42885637,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42885637,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42885637,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42885637,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42885637,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42885637,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42885637,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42885637,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42885637,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42885637,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42885637,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42885637,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42885637,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42885637,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42885637,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42885637,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16361
|
NULL
|
NULL
|
NULL
|
|
16367
|
735
|
13
|
2026-05-11T08:46:55.163362+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778489215163_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42885637,"top":0.09736632,"width":0.5711436,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42885637,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42885637,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42885637,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42885637,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42885637,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42885637,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42885637,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42885637,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42885637,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42885637,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42885637,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42885637,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42885637,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42885637,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42885637,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42885637,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42885637,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42885637,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42885637,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42885637,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42885637,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42885637,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42885637,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42885637,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16366
|
NULL
|
NULL
|
NULL
|
|
16511
|
741
|
11
|
2026-05-11T09:01:48.815884+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778490108815_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"bounds":{"left":0.124667555,"top":0.16919394,"width":0.34707448,"height":0.8308061},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42885637,"top":0.09736632,"width":0.5711436,"height":0.8818835},"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16510
|
NULL
|
NULL
|
NULL
|
|
16514
|
741
|
13
|
2026-05-11T09:01:51.839188+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778490111839_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42885637,"top":0.09736632,"width":0.5711436,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42885637,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42885637,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42885637,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42885637,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42885637,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42885637,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42885637,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42885637,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42885637,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42885637,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42885637,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42885637,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42885637,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42885637,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42885637,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42885637,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42885637,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42885637,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42885637,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42885637,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42885637,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42885637,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42885637,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42885637,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16513
|
NULL
|
NULL
|
NULL
|
|
16515
|
740
|
13
|
2026-05-11T09:01:52.464804+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778490112464_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16517
|
741
|
15
|
2026-05-11T09:01:55.862141+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778490115862_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.6296542,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6409575,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.64827126,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.42885637,"top":0.09736632,"width":0.5711436,"height":0.8818835},"on_screen":true,"lines":[{"char_start":207,"char_count":30,"bounds":{"left":0.42885637,"top":0.0,"width":0.07513298,"height":0.014365523}},{"char_start":237,"char_count":36,"bounds":{"left":0.42885637,"top":0.0,"width":0.09075798,"height":0.014365523}},{"char_start":273,"char_count":32,"bounds":{"left":0.42885637,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.42885637,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.42885637,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.42885637,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.42885637,"top":0.008778931,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.42885637,"top":0.026336791,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.42885637,"top":0.043894652,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.42885637,"top":0.061452515,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.42885637,"top":0.079010375,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.42885637,"top":0.096568234,"width":0.56515956,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.42885637,"top":0.11412609,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.42885637,"top":0.13168396,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.42885637,"top":0.14924182,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.42885637,"top":0.16679968,"width":0.52360374,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.42885637,"top":0.18435754,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.42885637,"top":0.2019154,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.42885637,"top":0.21947326,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.42885637,"top":0.23703113,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.42885637,"top":0.254589,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.42885637,"top":0.27214685,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.42885637,"top":0.2897047,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.42885637,"top":0.30726257,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
16516
|
NULL
|
NULL
|
NULL
|
|
16521
|
740
|
15
|
2026-05-11T09:02:05.869640+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778490125869_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7220615986847313571
|
6378616412348221796
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
18319
|
793
|
14
|
2026-05-11T11:11:01.351474+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778497861351_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-h PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-Iiyroledey© BatchSyncCollectole balchsynckealssec clientone© ClosedDealStagesS © RateLimitExceDealrielasservice.gc)Decorateacuivilv.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterc) Hubspot Tokenman© PayloadBuilder.phpC) RemotecrmobiectrP ResponseNormalizec) Service.onrC)SvncFieldAction.onC) SvncRelatedActivitC) WebhookSvncBatclv MintearationAorM AcceccorsConfigD DTO> M SiltersJobs• M ProcnectSoarchStreW service lralts© DataClient.php© DecorateActivity.ph(e)LocalSearch.onpu Localsearchinteria© RemoteSearch.phpc) Service.phpv W Listenersc) ConvertLeadActivitc) PurceLookuocache> M Metadata> Miarationa Pioedrivev Salesforce• D FieldsM OnnortunitvMatcheMOnnortunitvSvneStM ProsneetSearchStr.• M ServiceTraitcC) Client nhr© DecorateActivity.ph. Delete@biectsTrait© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.phplucaneTacts naccod. 12 (todav 12:02WindowC) CachedCrmServiceDecorator.ongT DeleteCrmEntityTrait.phpclass Client extends Basecllent impLements HubspotcllentintertaceSthis->executeRequest(fn • => $this->getNewInstance@->crm0->...);* atemplate T* apanam callable: T $apiCall* drexurn* ochrows kateL1m1ctxcepczonorivate function executeRequestcallable SapicaloScachekev = sthis->qetRateLim1tcacheKevo:ScachedRetrvAfter = Redis:.det Scachekev):1+ us strinoscachedRetrvAfter)vx 1s_numericScachedRetrvAfter)) <throw new RateLimitExcentiong'Hubspot rate limit (cached circuit-breaker)',Gint) ScachedRetrvAfter.tryneturn Canicalld} catch (Throwable $e) {if (Sthis->isHubspotRateLimit(Se)) {SretryAfter = $this->parseRetryAfter($e);Redis::setex($cacheKey, SretryAfter, (string) $retryAfter):Sthis->loq->warning('[Hubspot] Received 429 from API'. [ttoom idl= sth1s->cont10->team1d'config id'=> Sthis->config->qetIdO."retry after' => Sretrvafter= Sthis-›parseMessage(Se).= Se->qetMessageorthrow new RateLimitExcention( message: 'Hubspot returned 429' SretrvAfter. Se);throw Se:Hudspol/serwice.pnpProviderkateLimiter.phgA2/67 V3 APreparation tor kerl…. In oum100% 1• Mon 11 May 14:11:01&console [pRODlA console [EUiconsole [STAGINGIш 1 шuш1l I11= custom.log=laravel.log XCc W.*11: 08:4511local.INFO: Jiminny \Console\Commands\Command::run Memory usage before starting command 1"command": "mailbox:batch:retry-failed", "memoryBeforecon11: 08:451111•08•467|local.INFO: Jiminny \Console\Commands \Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","11:09:12] local.INFO: [MatchActivityCrmData] Starting CRM data matching {"activity":615092,"remote_search":true,"set_config11•09•121|local.INFO: [ EsUpdateTarget ] Update single target {"target":"activities","purpose":"searchable-observer-update","entityId":615092} {"correlation_i11[PHONE]1 INEO. AsvnclndateSlasticSeanch Entity added to Redis Zist "entityIyne" "activities" "entityId" :615092"welueeelolnKevmeepeerelelen11•00•121local.INFO: [MatchActivityCrmData] Participants old state {"activity":615092,"participants":[{"id":1004102,"user_id":null,"contact_id":null,"lead_idSocialAccountCenvicol Cetchina token «icocialAcolocal.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"p)11:09:12] local.INF0: [EncryptedTokenManager] Generating access toker11.07.141Local. Inru.urmuwnerkesolver Incegracion owner macched as ukn uwner crnoroowner":148,"team_id":2} {"correlation_ id":"f33d.11:09:120[Prospect match] Cache miss11:04:151local.INF0: [Hubspot] Failed to fetch contact {"email":"nikolay.nikolov@jimin11:04:151local.INF0: [Prospect match] API returned empty result11:04:15|10:09:1501local.WARNING: [Hubspot] Nolocal.WARNING: [Hubspotl ReceivedExceptions\BadRequest""me'POST [URL_WITH_CREDENTIALS] local.INF0: [Prospect matchl cached empty result - no APT calls. try next matching method {"identifier type"."email" "identifier"."nikolTuondlolnbeno+Dotol&md+11: 09:141LocaL.INF0: Matchactzvityci11:09:141111: 09:141Asvncuodate.l10: 09 :14110: 09 :141LocaLINFO:SoctalAccounts11: 09• 14711: 09•147Local.TNF0lEncrvotedTokeni11: 09:141 Local. TNF0CrmOwnerResolv...
|
NULL
|
-7219240454161278196
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-h PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-Iiyroledey© BatchSyncCollectole balchsynckealssec clientone© ClosedDealStagesS © RateLimitExceDealrielasservice.gc)Decorateacuivilv.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterc) Hubspot Tokenman© PayloadBuilder.phpC) RemotecrmobiectrP ResponseNormalizec) Service.onrC)SvncFieldAction.onC) SvncRelatedActivitC) WebhookSvncBatclv MintearationAorM AcceccorsConfigD DTO> M SiltersJobs• M ProcnectSoarchStreW service lralts© DataClient.php© DecorateActivity.ph(e)LocalSearch.onpu Localsearchinteria© RemoteSearch.phpc) Service.phpv W Listenersc) ConvertLeadActivitc) PurceLookuocache> M Metadata> Miarationa Pioedrivev Salesforce• D FieldsM OnnortunitvMatcheMOnnortunitvSvneStM ProsneetSearchStr.• M ServiceTraitcC) Client nhr© DecorateActivity.ph. Delete@biectsTrait© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.phplucaneTacts naccod. 12 (todav 12:02WindowC) CachedCrmServiceDecorator.ongT DeleteCrmEntityTrait.phpclass Client extends Basecllent impLements HubspotcllentintertaceSthis->executeRequest(fn • => $this->getNewInstance@->crm0->...);* atemplate T* apanam callable: T $apiCall* drexurn* ochrows kateL1m1ctxcepczonorivate function executeRequestcallable SapicaloScachekev = sthis->qetRateLim1tcacheKevo:ScachedRetrvAfter = Redis:.det Scachekev):1+ us strinoscachedRetrvAfter)vx 1s_numericScachedRetrvAfter)) <throw new RateLimitExcentiong'Hubspot rate limit (cached circuit-breaker)',Gint) ScachedRetrvAfter.tryneturn Canicalld} catch (Throwable $e) {if (Sthis->isHubspotRateLimit(Se)) {SretryAfter = $this->parseRetryAfter($e);Redis::setex($cacheKey, SretryAfter, (string) $retryAfter):Sthis->loq->warning('[Hubspot] Received 429 from API'. [ttoom idl= sth1s->cont10->team1d'config id'=> Sthis->config->qetIdO."retry after' => Sretrvafter= Sthis-›parseMessage(Se).= Se->qetMessageorthrow new RateLimitExcention( message: 'Hubspot returned 429' SretrvAfter. Se);throw Se:Hudspol/serwice.pnpProviderkateLimiter.phgA2/67 V3 APreparation tor kerl…. In oum100% 1• Mon 11 May 14:11:01&console [pRODlA console [EUiconsole [STAGINGIш 1 шuш1l I11= custom.log=laravel.log XCc W.*11: 08:4511local.INFO: Jiminny \Console\Commands\Command::run Memory usage before starting command 1"command": "mailbox:batch:retry-failed", "memoryBeforecon11: 08:451111•08•467|local.INFO: Jiminny \Console\Commands \Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","11:09:12] local.INFO: [MatchActivityCrmData] Starting CRM data matching {"activity":615092,"remote_search":true,"set_config11•09•121|local.INFO: [ EsUpdateTarget ] Update single target {"target":"activities","purpose":"searchable-observer-update","entityId":615092} {"correlation_i11[PHONE]1 INEO. AsvnclndateSlasticSeanch Entity added to Redis Zist "entityIyne" "activities" "entityId" :615092"welueeelolnKevmeepeerelelen11•00•121local.INFO: [MatchActivityCrmData] Participants old state {"activity":615092,"participants":[{"id":1004102,"user_id":null,"contact_id":null,"lead_idSocialAccountCenvicol Cetchina token «icocialAcolocal.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"p)11:09:12] local.INF0: [EncryptedTokenManager] Generating access toker11.07.141Local. Inru.urmuwnerkesolver Incegracion owner macched as ukn uwner crnoroowner":148,"team_id":2} {"correlation_ id":"f33d.11:09:120[Prospect match] Cache miss11:04:151local.INF0: [Hubspot] Failed to fetch contact {"email":"nikolay.nikolov@jimin11:04:151local.INF0: [Prospect match] API returned empty result11:04:15|10:09:1501local.WARNING: [Hubspot] Nolocal.WARNING: [Hubspotl ReceivedExceptions\BadRequest""me'POST [URL_WITH_CREDENTIALS] local.INF0: [Prospect matchl cached empty result - no APT calls. try next matching method {"identifier type"."email" "identifier"."nikolTuondlolnbeno+Dotol&md+11: 09:141LocaL.INF0: Matchactzvityci11:09:141111: 09:141Asvncuodate.l10: 09 :14110: 09 :141LocaLINFO:SoctalAccounts11: 09• 14711: 09•147Local.TNF0lEncrvotedTokeni11: 09:141 Local. TNF0CrmOwnerResolv...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
592
|
23
|
17
|
2026-05-07T07:19:02.173265+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778138342173_m1.jpg...
|
Firefox
|
Moje predplatné | Denník N — Personal
|
True
|
predplatne.dennikn.sk/subscriptions/subscriptions/ predplatne.dennikn.sk/subscriptions/subscriptions/my...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Moje predplatné | Denník N
Moje predplatné | Denník N
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Preskočiť hlavnú navigáciu
Preskočiť hlavnú navigáciu
Denník N
Denník N
Denník N
[EMAIL]
[EMAIL]
Predplatné
Predplatné
Platby
Platby
Newslettre
Newslettre
Témy a autori
Témy a autori
Adresa, faktúry
Adresa, faktúry
Nastavenia
Nastavenia
Odhlásiť
Odhlásiť
Predĺžiť predplatné
Predĺžiť predplatné
Zmeniť predplatné / Upgrade
Zmeniť predplatné / Upgrade
História predplatných
História predplatných
Predplatné
Od
Do
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / 4 týždne za 7,99 EUR
7. mája 2026, 17:59:50
4. júna 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / 4 týždne za 7,99 EUR
9. apríla 2026, 17:59:50
Aktuálne
7. mája 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / 4 týždne za 7,99 EUR
12. marca 2026, 17:59:50
9. apríla 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandard predplatné na 4 týždne za polovičnú cenu
12. februára 2026, 17:59:50
12. marca 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandard predplatné na 4 týždne za polovičnú cenu
15. januára 2026, 17:59:50
12. februára 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandard predplatné na 4 týždne za polovičnú cenu
18. decembra 2025, 17:59:50
15. januára 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Akciové štandard predplatné N 2025 / 12 týždňov za 1,00 EUR
25. septembra 2025, 17:59:50
18. decembra 2025, 17:59:50
Web
Štandard
Mobilné aplikácie
Týždeň
Štandard Denník N a .týždeň 1 mesiac (v3)
15. mája 2025, 19:52:30
15. júna 2025, 19:52:30
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac za 7,99 (inapp platba)
23. februára 2025, 6:36:33
23. marca 2025, 5:36:25
Web
Bonusové dvojdňové predplatné
2. augusta 2024, 23:02:05
4. augusta 2024, 23:02:05
Web
Mini N / 6 mesiacov so 6 € trial zľavou (Subscribe with Google)
2. februára 2024, 22:02:39
2. augusta 2024, 23:01:52
Web
Webové mini predplatné N / 4 týždne za 5,99 EUR
2. októbra 2023, 18:47:41
30. októbra 2023, 18:47:41
Web
Webové mini predplatné N / 4 týždne za 5,99 EUR
22. júna 2023, 17:21:19
20. júla 2023, 17:21:19
Web
Bonusové dvojdňové predplatné
18. apríla 2023, 16:51:04
20. apríla 2023, 16:51:04
Web
Webové mini predplatné N / rok za 47,90 EUR
18. apríla 2022, 16:50:06
18. apríla 2023, 16:50:06
Web
Webové mini predplatné N / 4 týždne
21. marca 2022, 16:50:06
18. apríla 2022, 16:50:06
Web
Webové mini predplatné N / 4 týždne
19. februára 2022, 21:59:22
19. marca 2022, 21:59:22
Web
Webové mini predplatné N / 4 týždne
3. januára 2022, 10:46:12
31. januára 2022, 10:46:12
Web
Webové mini predplatné N / 4 týždne
3. decembra 2021, 16:12:07
31. decembra 2021, 16:12:07
Web
Webové mini predplatné N / mesiac
29. októbra 2021, 6:47:26
29. novembra 2021, 6:47:26
Web
Webové mini predplatné N / mesiac
1. augusta 2021, 8:51:45
1. septembra 2021, 8:51:45
Web
Webové mini predplatné N / mesiac
24. júna 2021, 9:26:56
25. júla 2021, 9:26:56
Web
Webové mini predplatné N / mesiac
23. mája 2021, 7:32:29
23. júna 2021, 7:32:29
Web
Webové mini predplatné N / mesiac
15. apríla 2021, 8:53:25
16. mája 2021, 8:53:25
Web
Webové mini predplatné N / mesiac
15. marca 2021, 8:53:25
15. apríla 2021, 8:53:25
Web
Webové mini predplatné N / mesiac
8. februára 2021, 4:12:51
11. marca 2021, 4:12:51
Web
Webové mini predplatné N / mesiac
8. januára 2021, 4:12:51
8. februára 2021, 4:12:51
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac
16. septembra 2020, 13:18:28
17. októbra 2020, 13:18:28
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac
16. augusta 2020, 8:37:44
16. septembra 2020, 8:37:44
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac
11. júna 2020, 6:49:27
12. júla 2020, 6:49:27
Web
Týždeň
Mesačné predplatné dennikn.sk a tyzden.sk
30. marca 2020, 13:07:55
29. apríla 2020, 13:07:55
Web
Týždeň
Mesačné predplatné dennikn.sk a tyzden.sk
28. februára 2020, 13:07:55
30. marca 2020, 13:07:55
Web
Webové mini predplatné N / mesiac
20. marca 2019, 6:27:23
20. apríla 2019, 6:27:23
Web
Webové mini predplatné N / mesiac
8. januára 2019, 20:28:39
8. februára 2019, 20:28:39
Web
Webové mini predplatné N / mesiac
15. novembra 2018, 8:22:57...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Home | Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home | Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login – Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login – Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Moje predplatné | Denník N","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Moje predplatné | Denník N","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Preskočiť hlavnú navigáciu","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Preskočiť hlavnú navigáciu","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Denník N","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"Denník N","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Denník N","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"kovaliklukas@gmail.com","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"kovaliklukas@gmail.com","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Predplatné","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Predplatné","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platby","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platby","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Newslettre","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Newslettre","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Témy a autori","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Témy a autori","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Adresa, faktúry","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Adresa, faktúry","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Nastavenia","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"Nastavenia","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Odhlásiť","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Odhlásiť","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":" Predĺžiť predplatné","depth":11,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Predĺžiť predplatné","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":" Zmeniť predplatné / Upgrade","depth":11,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Zmeniť predplatné / Upgrade","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"História predplatných","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"História predplatných","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Predplatné","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Od","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Do","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / 4 týždne za 7,99 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7. mája 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4. júna 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / 4 týždne za 7,99 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"9. apríla 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Aktuálne","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7. mája 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / 4 týždne za 7,99 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12. marca 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"9. apríla 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard predplatné na 4 týždne za polovičnú cenu","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12. februára 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12. marca 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard predplatné na 4 týždne za polovičnú cenu","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. januára 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12. februára 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard predplatné na 4 týždne za polovičnú cenu","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18. decembra 2025, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. januára 2026, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Akciové štandard predplatné N 2025 / 12 týždňov za 1,00 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"25. septembra 2025, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18. decembra 2025, 17:59:50","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Týždeň","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard Denník N a .týždeň 1 mesiac (v3)","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. mája 2025, 19:52:30","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. júna 2025, 19:52:30","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / mesiac za 7,99 (inapp platba)","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"23. februára 2025, 6:36:33","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"23. marca 2025, 5:36:25","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bonusové dvojdňové predplatné","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. augusta 2024, 23:02:05","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4. augusta 2024, 23:02:05","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mini N / 6 mesiacov so 6 € trial zľavou (Subscribe with Google)","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. februára 2024, 22:02:39","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. augusta 2024, 23:01:52","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / 4 týždne za 5,99 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. októbra 2023, 18:47:41","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"30. októbra 2023, 18:47:41","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / 4 týždne za 5,99 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22. júna 2023, 17:21:19","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"20. júla 2023, 17:21:19","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bonusové dvojdňové predplatné","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18. apríla 2023, 16:51:04","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"20. apríla 2023, 16:51:04","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / rok za 47,90 EUR","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18. apríla 2022, 16:50:06","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18. apríla 2023, 16:50:06","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / 4 týždne","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21. marca 2022, 16:50:06","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18. apríla 2022, 16:50:06","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / 4 týždne","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"19. februára 2022, 21:59:22","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"19. marca 2022, 21:59:22","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / 4 týždne","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. januára 2022, 10:46:12","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31. januára 2022, 10:46:12","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / 4 týždne","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. decembra 2021, 16:12:07","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31. decembra 2021, 16:12:07","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"29. októbra 2021, 6:47:26","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"29. novembra 2021, 6:47:26","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.0,"width":0.014930556,"height":0.014444444},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.0,"width":0.15729167,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. augusta 2021, 8:51:45","depth":13,"bounds":{"left":0.66180557,"top":0.0,"width":0.10763889,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. septembra 2021, 8:51:45","depth":13,"bounds":{"left":0.80972224,"top":0.0,"width":0.119791664,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.0,"width":0.014930556,"height":0.014444444},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.0,"width":0.15729167,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"24. júna 2021, 9:26:56","depth":13,"bounds":{"left":0.66180557,"top":0.0,"width":0.09652778,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"25. júla 2021, 9:26:56","depth":13,"bounds":{"left":0.80972224,"top":0.0,"width":0.09340278,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.0,"width":0.014930556,"height":0.014444444},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.0,"width":0.15729167,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"23. mája 2021, 7:32:29","depth":13,"bounds":{"left":0.66180557,"top":0.0,"width":0.099305555,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"23. júna 2021, 7:32:29","depth":13,"bounds":{"left":0.80972224,"top":0.0,"width":0.096875,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.035,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.031666666,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. apríla 2021, 8:53:25","depth":13,"bounds":{"left":0.66180557,"top":0.031666666,"width":0.10208333,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"16. mája 2021, 8:53:25","depth":13,"bounds":{"left":0.80972224,"top":0.031666666,"width":0.099305555,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.07611111,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.07277778,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. marca 2021, 8:53:25","depth":13,"bounds":{"left":0.66180557,"top":0.07277778,"width":0.10555556,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. apríla 2021, 8:53:25","depth":13,"bounds":{"left":0.80972224,"top":0.07277778,"width":0.10243055,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.11722222,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.11388889,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8. februára 2021, 4:12:51","depth":13,"bounds":{"left":0.66180557,"top":0.11388889,"width":0.109375,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11. marca 2021, 4:12:51","depth":13,"bounds":{"left":0.80972224,"top":0.11388889,"width":0.10555556,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.15833333,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.155,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8. januára 2021, 4:12:51","depth":13,"bounds":{"left":0.66180557,"top":0.155,"width":0.10486111,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8. februára 2021, 4:12:51","depth":13,"bounds":{"left":0.80972224,"top":0.155,"width":0.109375,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.19944444,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"bounds":{"left":0.29409721,"top":0.19944444,"width":0.031597223,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"bounds":{"left":0.33715278,"top":0.19944444,"width":0.06111111,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / mesiac","depth":13,"bounds":{"left":0.40243056,"top":0.19611111,"width":0.15208334,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"16. septembra 2020, 13:18:28","depth":13,"bounds":{"left":0.66180557,"top":0.19611111,"width":0.13055556,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"17. októbra 2020, 13:18:28","depth":13,"bounds":{"left":0.80972224,"top":0.19611111,"width":0.11736111,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.24055555,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"bounds":{"left":0.29409721,"top":0.24055555,"width":0.031597223,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"bounds":{"left":0.33715278,"top":0.24055555,"width":0.06111111,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / mesiac","depth":13,"bounds":{"left":0.40243056,"top":0.23722222,"width":0.15208334,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"16. augusta 2020, 8:37:44","depth":13,"bounds":{"left":0.66180557,"top":0.23722222,"width":0.11319444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"16. septembra 2020, 8:37:44","depth":13,"bounds":{"left":0.80972224,"top":0.23722222,"width":0.08993056,"height":0.040555555},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.3038889,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandard","depth":13,"bounds":{"left":0.29409721,"top":0.3038889,"width":0.031597223,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mobilné aplikácie","depth":13,"bounds":{"left":0.33715278,"top":0.3038889,"width":0.06111111,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Štandardné predplatné N / mesiac","depth":13,"bounds":{"left":0.40243056,"top":0.30055556,"width":0.15208334,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11. júna 2020, 6:49:27","depth":13,"bounds":{"left":0.66180557,"top":0.30055556,"width":0.09652778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12. júla 2020, 6:49:27","depth":13,"bounds":{"left":0.80972224,"top":0.30055556,"width":0.09340278,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.345,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Týždeň","depth":13,"bounds":{"left":0.29409721,"top":0.345,"width":0.025,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mesačné predplatné dennikn.sk a tyzden.sk","depth":13,"bounds":{"left":0.3236111,"top":0.34166667,"width":0.19340278,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"30. marca 2020, 13:07:55","depth":13,"bounds":{"left":0.66180557,"top":0.34166667,"width":0.11076389,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"29. apríla 2020, 13:07:55","depth":13,"bounds":{"left":0.80972224,"top":0.34166667,"width":0.10763889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.3861111,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Týždeň","depth":13,"bounds":{"left":0.29409721,"top":0.3861111,"width":0.025,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mesačné predplatné dennikn.sk a tyzden.sk","depth":13,"bounds":{"left":0.3236111,"top":0.38277778,"width":0.19340278,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28. februára 2020, 13:07:55","depth":13,"bounds":{"left":0.66180557,"top":0.38277778,"width":0.12013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"30. marca 2020, 13:07:55","depth":13,"bounds":{"left":0.80972224,"top":0.38277778,"width":0.11111111,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.42722222,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.4238889,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"20. marca 2019, 6:27:23","depth":13,"bounds":{"left":0.66180557,"top":0.4238889,"width":0.10555556,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"20. apríla 2019, 6:27:23","depth":13,"bounds":{"left":0.80972224,"top":0.4238889,"width":0.10243055,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.46833333,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.465,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8. januára 2019, 20:28:39","depth":13,"bounds":{"left":0.66180557,"top":0.465,"width":0.110416666,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8. februára 2019, 20:28:39","depth":13,"bounds":{"left":0.80972224,"top":0.465,"width":0.114930555,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Web","depth":13,"bounds":{"left":0.26770833,"top":0.5094444,"width":0.014930556,"height":0.014444444},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Webové mini predplatné N / mesiac","depth":13,"bounds":{"left":0.28715277,"top":0.5061111,"width":0.15729167,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"15. novembra 2018, 8:22:57","depth":13,"bounds":{"left":0.66180557,"top":0.5061111,"width":0.121875,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-7219056148449653512
|
2035227079913771883
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Moje predplatné | Denník N
Moje predplatné | Denník N
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Preskočiť hlavnú navigáciu
Preskočiť hlavnú navigáciu
Denník N
Denník N
Denník N
[EMAIL]
[EMAIL]
Predplatné
Predplatné
Platby
Platby
Newslettre
Newslettre
Témy a autori
Témy a autori
Adresa, faktúry
Adresa, faktúry
Nastavenia
Nastavenia
Odhlásiť
Odhlásiť
Predĺžiť predplatné
Predĺžiť predplatné
Zmeniť predplatné / Upgrade
Zmeniť predplatné / Upgrade
História predplatných
História predplatných
Predplatné
Od
Do
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / 4 týždne za 7,99 EUR
7. mája 2026, 17:59:50
4. júna 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / 4 týždne za 7,99 EUR
9. apríla 2026, 17:59:50
Aktuálne
7. mája 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / 4 týždne za 7,99 EUR
12. marca 2026, 17:59:50
9. apríla 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandard predplatné na 4 týždne za polovičnú cenu
12. februára 2026, 17:59:50
12. marca 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandard predplatné na 4 týždne za polovičnú cenu
15. januára 2026, 17:59:50
12. februára 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Štandard predplatné na 4 týždne za polovičnú cenu
18. decembra 2025, 17:59:50
15. januára 2026, 17:59:50
Web
Štandard
Mobilné aplikácie
Akciové štandard predplatné N 2025 / 12 týždňov za 1,00 EUR
25. septembra 2025, 17:59:50
18. decembra 2025, 17:59:50
Web
Štandard
Mobilné aplikácie
Týždeň
Štandard Denník N a .týždeň 1 mesiac (v3)
15. mája 2025, 19:52:30
15. júna 2025, 19:52:30
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac za 7,99 (inapp platba)
23. februára 2025, 6:36:33
23. marca 2025, 5:36:25
Web
Bonusové dvojdňové predplatné
2. augusta 2024, 23:02:05
4. augusta 2024, 23:02:05
Web
Mini N / 6 mesiacov so 6 € trial zľavou (Subscribe with Google)
2. februára 2024, 22:02:39
2. augusta 2024, 23:01:52
Web
Webové mini predplatné N / 4 týždne za 5,99 EUR
2. októbra 2023, 18:47:41
30. októbra 2023, 18:47:41
Web
Webové mini predplatné N / 4 týždne za 5,99 EUR
22. júna 2023, 17:21:19
20. júla 2023, 17:21:19
Web
Bonusové dvojdňové predplatné
18. apríla 2023, 16:51:04
20. apríla 2023, 16:51:04
Web
Webové mini predplatné N / rok za 47,90 EUR
18. apríla 2022, 16:50:06
18. apríla 2023, 16:50:06
Web
Webové mini predplatné N / 4 týždne
21. marca 2022, 16:50:06
18. apríla 2022, 16:50:06
Web
Webové mini predplatné N / 4 týždne
19. februára 2022, 21:59:22
19. marca 2022, 21:59:22
Web
Webové mini predplatné N / 4 týždne
3. januára 2022, 10:46:12
31. januára 2022, 10:46:12
Web
Webové mini predplatné N / 4 týždne
3. decembra 2021, 16:12:07
31. decembra 2021, 16:12:07
Web
Webové mini predplatné N / mesiac
29. októbra 2021, 6:47:26
29. novembra 2021, 6:47:26
Web
Webové mini predplatné N / mesiac
1. augusta 2021, 8:51:45
1. septembra 2021, 8:51:45
Web
Webové mini predplatné N / mesiac
24. júna 2021, 9:26:56
25. júla 2021, 9:26:56
Web
Webové mini predplatné N / mesiac
23. mája 2021, 7:32:29
23. júna 2021, 7:32:29
Web
Webové mini predplatné N / mesiac
15. apríla 2021, 8:53:25
16. mája 2021, 8:53:25
Web
Webové mini predplatné N / mesiac
15. marca 2021, 8:53:25
15. apríla 2021, 8:53:25
Web
Webové mini predplatné N / mesiac
8. februára 2021, 4:12:51
11. marca 2021, 4:12:51
Web
Webové mini predplatné N / mesiac
8. januára 2021, 4:12:51
8. februára 2021, 4:12:51
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac
16. septembra 2020, 13:18:28
17. októbra 2020, 13:18:28
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac
16. augusta 2020, 8:37:44
16. septembra 2020, 8:37:44
Web
Štandard
Mobilné aplikácie
Štandardné predplatné N / mesiac
11. júna 2020, 6:49:27
12. júla 2020, 6:49:27
Web
Týždeň
Mesačné predplatné dennikn.sk a tyzden.sk
30. marca 2020, 13:07:55
29. apríla 2020, 13:07:55
Web
Týždeň
Mesačné predplatné dennikn.sk a tyzden.sk
28. februára 2020, 13:07:55
30. marca 2020, 13:07:55
Web
Webové mini predplatné N / mesiac
20. marca 2019, 6:27:23
20. apríla 2019, 6:27:23
Web
Webové mini predplatné N / mesiac
8. januára 2019, 20:28:39
8. februára 2019, 20:28:39
Web
Webové mini predplatné N / mesiac
15. novembra 2018, 8:22:57...
|
588
|
NULL
|
NULL
|
NULL
|
|
6459
|
278
|
12
|
2026-05-08T06:28:00.685661+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778221680685_m2.jpg...
|
Firefox
|
SevenShores\Hubspot\Exceptions\BadRequest: Client SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT — Work...
|
True
|
jiminny.sentry.io/issues/7007366572/?environment=p jiminny.sentry.io/issues/7007366572/?environment=production&environment=production-eu&project=82419&query=is%3Aunresolved&referrer=issue-stream&sort=freq...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Close tab
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Illuminate\Queue\MaxAttemptsExceededException: Jiminny\Jobs\Activity\DeleteTeamChurnData has been attempted too many times. — jiminny — app
Illuminate\Queue\MaxAttemptsExceededException: Jiminny\Jobs\Activity\DeleteTeamChurnData has been attempted too many times. — jiminny — app
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Problem loading page
Problem loading page
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
Jiminny
Jiminny
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to main content
Skip to main content
Toggle organization menu
Issues
Issues
Explore
Explore
Dashboards
Dashboards
Monitors
Monitors
Settings
Settings
Try Business
What's New
Help
[EMAIL]
Issues
Expand
Feed
Feed
Errors & Outages
Errors & Outages
Breached Metrics
Breached Metrics
Warnings
Warnings
User Feedback
User Feedback
Autofix
Autofix
Recently Run
Recently Run
All Views
All Views
Configure
Alerts Moved
Alerts
Moved
Issues
Issues
View Project Details
APP-1EED
Ask Seer
Ask Seer
/
Give Feedback
SevenShores\Hubspot\Exceptions\BadRequest
View events
Events (total)
Users (90d)
Level: Error
Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019e024f-c (truncated...)
17K
0
Ongoing
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Resolve
Resolve
More resolve options
Archive
Archive
Archive options
Subscribe
Share
More Actions
Priority
Modify issue priority
High
Assignee
Modify issue assignee
Lukas Kovalik
production, production-eu
production, production-eu
90D
90D
Add a search term
Add a search term
Close sidebar
Toggle graph series - Events
Events
17K
Toggle graph series - Users
Users
0
release 68% 874599
release
68%
874599
environment 92% production
environment
92%
production
server_name 5% 1afcc19ab21f
server_name
5%
1afcc19ab21f
correlation_id <1% d59f2a2d-61c7-491a-9859-b5d9aec02eac
correlation_id
<1%
d59f2a2d-61c7-491a-9859-b5d9aec02eac
View all tags
View all tags
Select issue content
Events
Previous Event
Next Event
First
First
First
Latest
Latest
Latest
Recommended
Recommended
View More Events
View More Events
Copy as
Copy as
ID: 31c8b6c9
18 hours ago
JSON
JSON
Highlights
Highlights
Stack Trace
Stack Trace
Trace
Trace
Tags
Tags
Context
Context
php
8.3.30
Linux
6.1.164-196.303.amzn2023.aarch64
882311
882311
production-eu
Collapse Highlights Section
Highlights
Edit
Edit
handled
yes
level...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.34773937,"top":0.0518755,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.36103722,"top":0.06304868,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"bounds":{"left":0.34773937,"top":0.08459697,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"bounds":{"left":0.36103722,"top":0.09577015,"width":0.4644282,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.41505983,"top":0.09177973,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"bounds":{"left":0.34773937,"top":0.11731844,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":5,"bounds":{"left":0.36103722,"top":0.12849163,"width":0.10721409,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app","depth":4,"bounds":{"left":0.34773937,"top":0.15003991,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app","depth":5,"bounds":{"left":0.36103722,"top":0.16121309,"width":0.17037898,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Illuminate\\Queue\\MaxAttemptsExceededException: Jiminny\\Jobs\\Activity\\DeleteTeamChurnData has been attempted too many times. — jiminny — app","depth":4,"bounds":{"left":0.34773937,"top":0.18276137,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Illuminate\\Queue\\MaxAttemptsExceededException: Jiminny\\Jobs\\Activity\\DeleteTeamChurnData has been attempted too many times. — jiminny — app","depth":5,"bounds":{"left":0.36103722,"top":0.19393456,"width":0.2606383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.34773937,"top":0.21548285,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.36103722,"top":0.22665602,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Ask Jiminny Report Generated","depth":4,"bounds":{"left":0.34773937,"top":0.2482043,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Ask Jiminny Report Generated","depth":5,"bounds":{"left":0.36103722,"top":0.25937748,"width":0.07164229,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.34773937,"top":0.28092578,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.36103722,"top":0.29209897,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Problem loading page","depth":4,"bounds":{"left":0.34773937,"top":0.31364724,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Problem loading page","depth":5,"bounds":{"left":0.36103722,"top":0.32482043,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Search the CRM - HubSpot docs","depth":4,"bounds":{"left":0.34773937,"top":0.3463687,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Search the CRM - HubSpot docs","depth":5,"bounds":{"left":0.36103722,"top":0.3575419,"width":0.05651596,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.34773937,"top":0.3790902,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.36103722,"top":0.39026338,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.35056517,"top":0.41340783,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.35056517,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.3615359,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.3726729,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.38380983,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.3949468,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to main content","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to main content","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Toggle organization menu","depth":11,"bounds":{"left":0.43417552,"top":0.059856344,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues","depth":12,"bounds":{"left":0.42869017,"top":0.09736632,"width":0.021609042,"height":0.050678372},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":14,"bounds":{"left":0.43434176,"top":0.13048683,"width":0.010305851,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Explore","depth":12,"bounds":{"left":0.42869017,"top":0.14804469,"width":0.021609042,"height":0.050678372},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Explore","depth":14,"bounds":{"left":0.43351063,"top":0.1811652,"width":0.011968086,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Dashboards","depth":12,"bounds":{"left":0.42869017,"top":0.19872306,"width":0.021609042,"height":0.050678372},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Dashboards","depth":14,"bounds":{"left":0.42985374,"top":0.23184358,"width":0.019281914,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Monitors","depth":12,"bounds":{"left":0.42869017,"top":0.24940144,"width":0.021609042,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Monitors","depth":14,"bounds":{"left":0.4325133,"top":0.28252193,"width":0.013962766,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"bounds":{"left":0.42869017,"top":0.29968077,"width":0.021609042,"height":0.050678372},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.43267953,"top":0.33280128,"width":0.013630319,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Try Business","depth":10,"bounds":{"left":0.43417552,"top":0.88667196,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"What's New","depth":10,"bounds":{"left":0.43417552,"top":0.9114126,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":10,"bounds":{"left":0.43417552,"top":0.93615323,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"lukas.kovalik@jiminny.com","depth":10,"bounds":{"left":0.43417552,"top":0.9680766,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Issues","depth":13,"bounds":{"left":0.39079124,"top":0.066640064,"width":0.014461436,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Expand","depth":13,"bounds":{"left":0.43633643,"top":0.061452515,"width":0.00930851,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Feed","depth":15,"bounds":{"left":0.38746676,"top":0.10055866,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed","depth":17,"bounds":{"left":0.39178857,"top":0.10734238,"width":0.010638298,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Errors & Outages","depth":15,"bounds":{"left":0.38746676,"top":0.14046289,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Errors & Outages","depth":17,"bounds":{"left":0.39178857,"top":0.14724661,"width":0.03673537,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Breached Metrics","depth":15,"bounds":{"left":0.38746676,"top":0.16759777,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Breached Metrics","depth":17,"bounds":{"left":0.39178857,"top":0.17438148,"width":0.037898935,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Warnings","depth":15,"bounds":{"left":0.38746676,"top":0.19473264,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Warnings","depth":17,"bounds":{"left":0.39178857,"top":0.20151636,"width":0.019946808,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"User Feedback","depth":15,"bounds":{"left":0.38746676,"top":0.22186752,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"User Feedback","depth":17,"bounds":{"left":0.39178857,"top":0.22865124,"width":0.032081116,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Autofix","depth":13,"bounds":{"left":0.38746676,"top":0.26177174,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Autofix","depth":16,"bounds":{"left":0.39145613,"top":0.26855546,"width":0.016289894,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Recently Run","depth":15,"bounds":{"left":0.38746676,"top":0.28731045,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Recently Run","depth":17,"bounds":{"left":0.39178857,"top":0.29409418,"width":0.028922873,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"All Views","depth":15,"bounds":{"left":0.38746676,"top":0.3272147,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All Views","depth":17,"bounds":{"left":0.39178857,"top":0.3339984,"width":0.019281914,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Configure","depth":14,"bounds":{"left":0.39145613,"top":0.3735036,"width":0.021941489,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alerts Moved","depth":15,"bounds":{"left":0.38746676,"top":0.39225858,"width":0.058843084,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alerts","depth":17,"bounds":{"left":0.39178857,"top":0.3990423,"width":0.012799202,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Moved","depth":17,"bounds":{"left":0.42819148,"top":0.39984038,"width":0.012466756,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Issues","depth":12,"bounds":{"left":0.45728058,"top":0.0650439,"width":0.013796543,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":14,"bounds":{"left":0.45728058,"top":0.066640064,"width":0.013796543,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View Project Details","depth":13,"bounds":{"left":0.47772607,"top":0.06624102,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"APP-1EED","depth":16,"bounds":{"left":0.48570478,"top":0.066640064,"width":0.021941489,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Ask Seer","depth":10,"bounds":{"left":0.93484044,"top":0.059856344,"width":0.04720745,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Seer","depth":13,"bounds":{"left":0.9461436,"top":0.0650439,"width":0.019614361,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":14,"bounds":{"left":0.9740692,"top":0.065442935,"width":0.0021609042,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Give Feedback","depth":11,"bounds":{"left":0.9840425,"top":0.059856344,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View events","depth":13,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Events (total)","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Users (90d)","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Level: Error","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e024f-c (truncated...)","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"17K","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"0","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Ongoing","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Resolve","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Resolve","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More resolve options","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Archive","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Archive","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Archive options","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Subscribe","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Share","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More Actions","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Priority","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Modify issue priority","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"High","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Assignee","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Modify issue assignee","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"production, production-eu","depth":13,"on_screen":false,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"production, production-eu","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"90D","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"90D","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXComboBox","text":"Add a search term","depth":16,"on_screen":false,"help_text":"","placeholder":"Filter events…","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"Add a search term","depth":16,"on_screen":false,"help_text":"","placeholder":"Filter events…","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close sidebar","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Toggle graph series - Events","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Events","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"17K","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Toggle graph series - Users","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Users","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"0","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"release 68% 874599","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"release","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"68%","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"874599","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"environment 92% production","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"environment","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"92%","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"production","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"server_name 5% 1afcc19ab21f","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"server_name","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5%","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1afcc19ab21f","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"correlation_id <1% d59f2a2d-61c7-491a-9859-b5d9aec02eac","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"correlation_id","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"<1%","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"d59f2a2d-61c7-491a-9859-b5d9aec02eac","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View all tags","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View all tags","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Select issue content","depth":13,"on_screen":false,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Events","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Previous Event","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Next Event","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"First","depth":14,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"First","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"First","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Latest","depth":14,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Latest","depth":15,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Latest","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Recommended","depth":14,"on_screen":false,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Recommended","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"View More Events","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View More Events","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy as","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Copy as","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ID: 31c8b6c9","depth":15,"bounds":{"left":0.4616024,"top":0.105347164,"width":0.029089095,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"18 hours ago","depth":15,"bounds":{"left":0.5013298,"top":0.105347164,"width":0.027593086,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JSON","depth":14,"bounds":{"left":0.5337433,"top":0.10454908,"width":0.012300532,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JSON","depth":15,"bounds":{"left":0.5337433,"top":0.105347164,"width":0.012300532,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Highlights","depth":17,"bounds":{"left":0.7787567,"top":0.100159615,"width":0.024268618,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Highlights","depth":19,"bounds":{"left":0.78141624,"top":0.10614525,"width":0.018949468,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Stack Trace","depth":17,"bounds":{"left":0.80369014,"top":0.100159615,"width":0.026928192,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stack Trace","depth":19,"bounds":{"left":0.80634975,"top":0.10614525,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Trace","depth":17,"bounds":{"left":0.8312833,"top":0.100159615,"width":0.015292553,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Trace","depth":19,"bounds":{"left":0.83394283,"top":0.10614525,"width":0.009973404,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Tags","depth":17,"bounds":{"left":0.8472407,"top":0.100159615,"width":0.013962766,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Tags","depth":19,"bounds":{"left":0.84990025,"top":0.10614525,"width":0.008643617,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Context","depth":17,"bounds":{"left":0.8618683,"top":0.100159615,"width":0.020113032,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Context","depth":19,"bounds":{"left":0.86452794,"top":0.10614525,"width":0.014793883,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"php","depth":16,"bounds":{"left":0.46958113,"top":0.0,"width":0.00831117,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8.3.30","depth":16,"bounds":{"left":0.47988698,"top":0.0,"width":0.013962766,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Linux","depth":16,"bounds":{"left":0.5071476,"top":0.0,"width":0.011801862,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6.1.164-196.303.amzn2023.aarch64","depth":16,"bounds":{"left":0.5209442,"top":0.0,"width":0.076961435,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"882311","depth":17,"bounds":{"left":0.61053854,"top":0.0,"width":0.015458777,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"882311","depth":18,"bounds":{"left":0.61053854,"top":0.0,"width":0.015458777,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"production-eu","depth":17,"bounds":{"left":0.63863033,"top":0.0,"width":0.031416222,"height":0.012370312},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse Highlights Section","depth":14,"bounds":{"left":0.4616024,"top":0.0,"width":0.39744017,"height":0.028731046},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Highlights","depth":17,"bounds":{"left":0.47024602,"top":0.0043894653,"width":0.026595745,"height":0.01396648},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit","depth":14,"bounds":{"left":0.86170214,"top":0.0,"width":0.018949468,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Edit","depth":16,"bounds":{"left":0.8703458,"top":0.0059856344,"width":0.0076462766,"height":0.010774142},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"handled","depth":16,"bounds":{"left":0.47024602,"top":0.033918597,"width":0.016788565,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"yes","depth":16,"bounds":{"left":0.53174865,"top":0.033918597,"width":0.0071476065,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"level","depth":16,"bounds":{"left":0.47024602,"top":0.051476456,"width":0.011968086,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-7218438623422420516
|
-3004886901297847104
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Close tab
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Illuminate\Queue\MaxAttemptsExceededException: Jiminny\Jobs\Activity\DeleteTeamChurnData has been attempted too many times. — jiminny — app
Illuminate\Queue\MaxAttemptsExceededException: Jiminny\Jobs\Activity\DeleteTeamChurnData has been attempted too many times. — jiminny — app
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Problem loading page
Problem loading page
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
Jiminny
Jiminny
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to main content
Skip to main content
Toggle organization menu
Issues
Issues
Explore
Explore
Dashboards
Dashboards
Monitors
Monitors
Settings
Settings
Try Business
What's New
Help
[EMAIL]
Issues
Expand
Feed
Feed
Errors & Outages
Errors & Outages
Breached Metrics
Breached Metrics
Warnings
Warnings
User Feedback
User Feedback
Autofix
Autofix
Recently Run
Recently Run
All Views
All Views
Configure
Alerts Moved
Alerts
Moved
Issues
Issues
View Project Details
APP-1EED
Ask Seer
Ask Seer
/
Give Feedback
SevenShores\Hubspot\Exceptions\BadRequest
View events
Events (total)
Users (90d)
Level: Error
Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019e024f-c (truncated...)
17K
0
Ongoing
/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php in Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::executeSearchRequest
Resolve
Resolve
More resolve options
Archive
Archive
Archive options
Subscribe
Share
More Actions
Priority
Modify issue priority
High
Assignee
Modify issue assignee
Lukas Kovalik
production, production-eu
production, production-eu
90D
90D
Add a search term
Add a search term
Close sidebar
Toggle graph series - Events
Events
17K
Toggle graph series - Users
Users
0
release 68% 874599
release
68%
874599
environment 92% production
environment
92%
production
server_name 5% 1afcc19ab21f
server_name
5%
1afcc19ab21f
correlation_id <1% d59f2a2d-61c7-491a-9859-b5d9aec02eac
correlation_id
<1%
d59f2a2d-61c7-491a-9859-b5d9aec02eac
View all tags
View all tags
Select issue content
Events
Previous Event
Next Event
First
First
First
Latest
Latest
Latest
Recommended
Recommended
View More Events
View More Events
Copy as
Copy as
ID: 31c8b6c9
18 hours ago
JSON
JSON
Highlights
Highlights
Stack Trace
Stack Trace
Trace
Trace
Tags
Tags
Context
Context
php
8.3.30
Linux
6.1.164-196.303.amzn2023.aarch64
882311
882311
production-eu
Collapse Highlights Section
Highlights
Edit
Edit
handled
yes
level...
|
6457
|
NULL
|
NULL
|
NULL
|