|
21052
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
No Folder Opened Section
NO FOLDER OPENED
NO FOLDER OPENED
Connected to remote.
Open Folder
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports
PORTS
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Downloading VS Code Server......
|
Code
|
Visual Studio Code
|
NULL
|
21052
|
|
21053
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
No Folder Opened Section
NO FOLDER OPENED
NO FOLDER OPENED
Connected to remote.
Open Folder
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports
PORTS
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Downloading VS Code Server...
Open Folder
/ho
30 Results
OK
Show Local
bin
bin
boot
boot
daemon.json
daemon.json
data
data
dev
dev
etc
etc
home
home
lib
lib
lib32...
|
Code
|
Visual Studio Code
|
NULL
|
21053
|
|
21054
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
No Folder Opened Section
NO FOLDER OPENED
NO FOLDER OPENED
Connected to remote.
Open Folder
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports
PORTS
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Downloading VS Code Server...
Open Folder
/vol
30 Results
OK
Show Local
home
home
lib
lib
lib32
lib32
lib64
lib64
libx32
libx32
media
media
mnt
mnt
opt
opt
overlay
overlay
proc
proc
rom
rom
root
root
rootfs
rootfs
run
run
runtime
runtime
sbin
sbin
srv
srv
sys
sys
tmp
tmp
ugreen
ugreen
usr
usr
var
var
volume1
volume1...
|
Code
|
Visual Studio Code
|
NULL
|
21054
|
|
21055
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
No Folder Opened Section
NO FOLDER OPENED
NO FOLDER OPENED
Connected to remote.
Open Folder
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports
PORTS
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Downloading VS Code Server...
Open Folder
/volume1/screenpipe/
7 Results
OK
Show Local
..
..
.git
.git
#recycle
#recycle
app
app
data
data
logs
logs
pipes
pipes...
|
Code
|
Visual Studio Code
|
NULL
|
21055
|
|
21056
|
Explorer (⇧⌘E)
selectionViewlerminalWindowmelp100% Explorer (⇧⌘E)
selectionViewlerminalWindowmelp100% C. Q. Mon 11 May 20:05:27env — screenpipe SSH: nasдeV SCREENPIPE [SSH: NAS]> #recyclevapoconsumers> static.env* cli.py# db.pv# docker-compose.vml* Dockerfile# main.pv=requirements.txt• data>logs→oinec• aitianore* aoo settinas.ison=archive.db=archive.db-bal= do salite= dh calite-chmE dh calite-wall$ screennine svnc undated shi$ screenpipe_sync.shE screennine dhs apo settinas.isonscreenpipe_sync.sh•pipe.md• .envapp> @.envANTHROPIC [ENV_SECRET] ...•4 C;Implement and run pipe I... XPROBLEMSOUTPUTDEBUG CONSOLE+v0u.laxOUTIING1 TIMELINE*SSH: nas@0A0taacalaSpaces: 4 UTF-8 LF Dotenv88 SignIn...
|
Code
|
.env — screenpipe [SSH: nas]
|
NULL
|
21056
|
|
21057
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21057
|
|
21058
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21058
|
|
21059
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21059
|
|
21060
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl [URL_WITH_CREDENTIALS] sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20
Running pipe 'day-recap' for 2026-04-20...
Traceback (most recent call last):
File "/app/cli.py", line 245, in <module>
cli()
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1161, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1082, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1697, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 788, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/cli.py", line 225, in pipe
result = run_pipe(pipe_id, target, pipes_dir=pipes_dir)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/consumers/pipes.py", line 130, in run_pipe
message = client.messages.create(
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_utils/_utils.py", line 275, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/resources/messages/messages.py", line 953, in create
return self._post(
^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1336, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1013, in request
return self._request(
^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1117, in _request
raise self._make_status_error_from_response(err.response) from None
anthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'invalid x-api-key'}, 'request_id': 'req_011CaSoR75WgV8fTcHTYJMDY'}
Adm1n@DXP4800PLUS-B5F8:/volume1/screenpipe/app$
Thinking
Thinking
The container started before you updated the
.env
, so it's using the old placeholder key. Restart it to pick up the new one:
Copy code to clipboard
cd /volume1/screenpipe/app
sudo docker compose restart screenpipe-app
Then try again:
Copy code to clipboard
sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20
Message actions
Adm1n@DXP4800PLUS-B5F8:/volume1/screenpipe/app$ sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20
Running pipe 'day-recap' for 2026-04-20...
Traceback (most recent call last):
File "/app/cli.py", line 245, in <module>
cli()
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1161, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1082, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1697, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 788, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/cli.py", line 225, in pipe
result = run_pipe(pipe_id, target, pipes_dir=pipes_dir)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/consumers/pipes.py", line 130, in run_pipe
message = client.messages.create(
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_utils/_utils.py", line 275, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/resources/messages/messages.py", line 953, in create
return self._post(
^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1336, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1013, in request
return self._request(
^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1117, in _request
raise self._make_status_error_from_response(err.response) from None
anthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'invalid x-api-key'}, 'request_id': 'req_011CaSoaWfbMTbPTbw5PpBu1'}
Adm1n@DXP4800PLUS-B5F8:/volume1/screenpipe/app$
Message actions...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21060
|
|
21061
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21061
|
|
21062
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in
_parse_frontmatter
and pass it through
run_pipe
. Want me to add that?
Message actions
pipe.md
pipe.md
can I use mistral or other llm? or is is build for antrophic only
Thinking
Thinking
Currently it's Anthropic-only — the code calls the
anthropic
SDK directly in both
consumers/pipes.py
consumers/pipes.py
and
consumers/summary.py
consumers/summary.py
.
Adding other providers is straightforward since Mistral, Ollama, OpenRouter, Groq, and most modern LLMs expose an OpenAI-compatible API. The cleanest approach: add the
openai
package and let you configure the provider via
.env
:
Copy code to clipboard
# Anthropic (default)
[ENV_SECRET]
# OR: any OpenAI-compatible provider
LLM_PROVIDER=openai-compatible
LLM_BASE_URL=https://api.mistral.ai/v1
[ENV_SECRET]
LLM_MODEL=mistral-small-latest
This would cover Mistral, Ollama (
http://localhost:11434/v1
), OpenRouter, Groq, etc. with one code path.
Want me to implement that? It's a small change — a
_call_llm(system, user_prompt)
helper in
pipes.py
that branches on
LLM_PROVIDER
, and
openai
added to
requirements.txt
.
Message actions
lets test it with anthropic first, how to run pipe
Thinking
Thinking
Bash
Check if container is running
IN
cd /volume1/screenpipe/app && docker compose ps 2>/dev/null || docker ps --filter name=screenpipe
Copy code to clipboard
OUT
Exit code 1
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json?filters=%7B%22name%22%3A%7B%22screenpipe%22%3Atrue%7D%7D": dial unix /var/run/docker.sock: connect: permission denied
I don't have Docker socket access, so you'll need to run these yourself. Here are your options:
Option A — via Docker (if container is already running):
Copy code to clipboard
# List available pipes
docker exec -it screenpipe-app python cli.py pipe --list
# Run a specific pipe
docker exec -it screenpipe-app python cli.py pipe day-recap
Option B — rebuild first (since we added new files), then run:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
docker exec -it screenpipe-app python cli.py pipe day-recap
Option C — via the API
(if container is up on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST http://localhost:8766/api/pipes/day-recap/run
The
day-recap
or
standup-update
pipes are the best ones to test first since they use all data sources. Which option works for you?
Message actions
how ot run for specifiday
Thinking
Thinking
Docker exec:
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-25
# or shortcuts:
docker exec -it screenpipe-app python cli.py pipe day-recap --date yesterday
docker exec -it screenpipe-app python cli.py pipe day-recap --date today
API:
Copy code to clipboard...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21062
|
|
21063
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21063
|
|
21064
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl [URL_WITH_CREDENTIALS] sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20
Running pipe 'day-recap' for 2026-04-20...
Traceback (most recent call last):
File "/app/cli.py", line 245, in <module>
cli()
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1161, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1082, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1697, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/click/core.py", line 788, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/cli.py", line 225, in pipe
result = run_pipe(pipe_id, target, pipes_dir=pipes_dir)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/consumers/pipes.py", line 130, in run_pipe
message = client.messages.create(
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_utils/_utils.py", line 275, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/resources/messages/messages.py", line 953, in create
return self._post(
^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1336, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1013, in request
return self._request(
^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/anthropic/_base_client.py", line 1117, in _request
raise self._make_status_error_from_response(err.response) from None
anthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'invalid x-api-key'}, 'request_id': 'req_011CaSoR75WgV8fTcHTYJMDY'}
Adm1n@DXP4800PLUS-B5F8:/volume1/screenpipe/app$
Thinking
Thinking...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21064
|
|
21065
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21065
|
|
21066
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21066
|
|
21067
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in
_parse_frontmatter
and pass it through
run_pipe
. Want me to add that?
Message actions
pipe.md
pipe.md
can I use mistral or other llm? or is is build for antrophic only
Thinking
Thinking
Currently it's Anthropic-only — the code calls the
anthropic
SDK directly in both
consumers/pipes.py
consumers/pipes.py
and
consumers/summary.py
consumers/summary.py
.
Adding other providers is straightforward since Mistral, Ollama, OpenRouter, Groq, and most modern LLMs expose an OpenAI-compatible API. The cleanest approach: add the
openai
package and let you configure the provider via
.env
:...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21067
|
|
21068
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21068
|
|
21069
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21069
|
|
21070
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21070
|
|
21071
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in
_parse_frontmatter
and pass it through
run_pipe
. Want me to add that?
Message actions
pipe.md
pipe.md
can I use mistral or other llm? or is is build for antrophic only
Thinking
Thinking
Currently it's Anthropic-only — the code calls the
anthropic
SDK directly in both
consumers/pipes.py
consumers/pipes.py
and
consumers/summary.py
consumers/summary.py
.
Adding other providers is straightforward since Mistral, Ollama, OpenRouter, Groq, and most modern LLMs expose an OpenAI-compatible API. The cleanest approach: add the...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21071
|
|
21072
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21072
|
|
21073
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in
_parse_frontmatter
and pass it through
run_pipe
. Want me to add that?
Message actions
pipe.md
pipe.md
can I use mistral or other llm? or is is build for antrophic only
Thinking
Thinking
Currently it's Anthropic-only — the code calls the
anthropic
SDK directly in both
consumers/pipes.py
consumers/pipes.py
and
consumers/summary.py
consumers/summary.py
.
Adding other providers is straightforward since Mistral, Ollama, OpenRouter, Groq, and most modern LLMs expose an OpenAI-compatible API. The cleanest approach: add the
openai
package and let you configure the provider via
.env
:
Copy code to clipboard
# Anthropic (default)
[ENV_SECRET]
# OR: any OpenAI-compatible provider
LLM_PROVIDER=openai-compatible
LLM_BASE_URL=https://api.mistral.ai/v1
[ENV_SECRET]
LLM_MODEL=mistral-small-latest
This would cover Mistral, Ollama (
http://localhost:11434/v1
), OpenRouter, Groq, etc. with one code path.
Want me to implement that? It's a small change — a
_call_llm(system, user_prompt)
helper in
pipes.py
that branches on
LLM_PROVIDER...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21073
|
|
21074
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21074
|
|
21075
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21075
|
|
21076
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in
_parse_frontmatter
and pass it through
run_pipe
. Want me to add that?
Message actions
pipe.md
pipe.md
can I use mistral or other llm? or is is build for antrophic only
Thinking
Thinking
Currently it's Anthropic-only — the code calls the
anthropic
SDK directly in both
consumers/pipes.py
consumers/pipes.py
and
consumers/summary.py
consumers/summary.py
.
Adding other providers is straightforward since Mistral, Ollama, OpenRouter, Groq, and most modern LLMs expose an OpenAI-compatible API. The cleanest approach: add the
openai
package and let you configure the provider via
.env
:
Copy code to clipboard
# Anthropic (default)
[ENV_SECRET]
# OR: any OpenAI-compatible provider
LLM_PROVIDER=openai-compatible
LLM_BASE_URL=https://api.mistral.ai/v1
[ENV_SECRET]
LLM_MODEL=mistral-small-latest
This would cover Mistral, Ollama (
http://localhost:11434/v1
), OpenRouter, Groq, etc. with one code path.
Want me to implement that? It's a small change — a
_call_llm(system, user_prompt)
helper in
pipes.py
that branches on
LLM_PROVIDER
, and
openai
added to
requirements.txt
.
Message actions
lets test it with anthropic first, how to run pipe
Thinking
Thinking
Bash
Check if container is running
IN
cd /volume1/screenpipe/app && docker compose ps 2>/dev/null || docker ps --filter name=screenpipe
Copy code to clipboard
OUT
Exit code 1
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json?filters=%7B%22name%22%3A%7B%22screenpipe%22%3Atrue%7D%7D": dial unix /var/run/docker.sock: connect: permission denied
I don't have Docker socket access, so you'll need to run these yourself. Here are your options:
Option A — via Docker (if container is already running):
Copy code to clipboard
# List available pipes
docker exec -it screenpipe-app python cli.py pipe --list
# Run a specific pipe
docker exec -it screenpipe-app python cli.py pipe day-recap
Option B — rebuild first (since we added new files), then run:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
docker exec -it screenpipe-app python cli.py pipe day-recap
Option C — via the API
(if container is up on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST http://localhost:8766/api/pipes/day-recap/run
The...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21076
|
|
21077
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21077
|
|
21078
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
[ENV_SECRET]
[ENV_SECRET]
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]
Where to place the key:
app/.env
app/.env
— replace
your_key_here
with your actual key:
Copy code to clipboard
[ENV_SECRET]
That file is loaded by
load_dotenv()
at startup and is excluded from git via
.gitignore
.
Can you use different keys per pipe?
Not currently — all pipes share the one key from
.env
. But you can add per-pipe key support by adding an
api_key
field to the pipe's frontmatter:
Copy code to clipboard
---
title: Meeting Summary
api_key_env: ANTHROPIC_KEY_MEETINGS
---
Then in
consumers/pipes.py
consumers/pipes.py
, read it in
_parse_frontmatter
and pass it through
run_pipe
. Want me to add that?
Message actions
pipe.md
pipe.md
can I use mistral or other llm? or is is build for antrophic only
Thinking
Thinking
Currently it's Anthropic-only — the code calls the
anthropic
SDK directly in both...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21078
|
|
21079
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21079
|
|
21080
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21080
|
|
21081
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
screenpipe_sync.sh, Editor Group 1
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 30, Col 2
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta, body
def load_pipes(pipes_dir: Path) -> list[dict]:
if not pipes_dir.exists():
return []
pipes = []
for pipe_dir in sorted(pipes_dir.iterdir()):
if not pipe_dir.is_dir() or pipe_dir.name.startswith("."):
continue
pipe_md = pipe_dir / "pipe.md"
if not pipe_md.exists():
continue
text = pipe_md.read_text(encoding="utf-8")
meta, body = _parse_frontmatter(text)
if meta.get("enabled", "true").lower() == "false":
continue
body = re.sub(r"Read screenpipe skill first\.?\n?", "", body).strip()
pipes.append({
"id": pipe_dir.name,
"title": meta.get("title", pipe_dir.name),
"description": meta.get("description", ""),
"icon": meta.get("icon", ""),
"schedule": meta.get("schedule", "manual"),
"featured": meta.get("featured", "false").lower() == "true",
"prompt": body,
})
return pipes
def _build_system(d: date, stats: dict) -> str:
from consumers.activity import app_time, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
apps = app_time(d)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(d, limit=20)
url_lines = "\n".join(f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls)
ui_rows = ui_event_summary(d)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(d, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(d)
if len(audio) > 3000:
audio = audio[:3000]
return f"""You are analyzing screenpipe data — recordings of a person's computer activity.
## App usage (minutes per app):
{app_lines or '(no app data)'}
## Browser URLs visited:
{url_lines or '(no browser data)'}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_lines or '(no UI event data)'}
## Screen content (sampled every 5 minutes):
{ocr_lines or '(no screen data)'}
## Audio / speech:
{audio or '(no audio data)'}
Date: {d}
Total frames recorded: {stats['total_frames']}"""
def run_pipe(
pipe_id: str,
for_date: date | None = None,
api_key: str | None = None,
pipes_dir: Path | None = None,
) -> dict[str, Any]:
import anthropic
from consumers.activity import summary_stats
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {"title": "Error", "content": "ANTHROPIC_API_KEY not set.", "date": str(d)}
_pipes_dir = pipes_dir or Path(os.environ.get("PIPES_DIR", "/data/pipes"))
all_pipes = {p["id"]: p for p in load_pipes(_pipes_dir)}
if pipe_id not in all_pipes:
return {"title": "Error", "content": f"Pipe not found: {pipe_id}", "date": str(d)}
pipe = all_pipes[pipe_id]
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {"title": pipe["title"], "content": f"No screen data found for {d}.", "date": str(d)}
system = _build_system(d, stats)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": pipe["prompt"]}],
)
return {
"title": pipe["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Now add the API endpoints to
main.py
:
Edit
main.py
main.py
Added 1 line
Edit
main.py
main.py
Added 1 line
Now add the pipe endpoints at the end of main.py, before the closing summary section:
Edit
main.py
main.py
Added 14 lines
Now add the CLI command to
cli.py
cli.py
:
Edit
cli.py
cli.py
Added 40 lines
Done. Here's how to run pipes:
How to run pipes
How to run pipes
Via Docker (inside the running container):
Copy code to clipboard
docker exec -it screenpipe-app python cli.py pipe --list
docker exec -it screenpipe-app python cli.py pipe day-recap
docker exec -it screenpipe-app python cli.py pipe standup-update --date yesterday
Via the API
(once the container is running on port 8766):
Copy code to clipboard
# List pipes
curl http://localhost:8766/api/pipes
# Run a pipe
curl -X POST "http://localhost:8766/api/pipes/day-recap/run"
curl -X POST "http://localhost:8766/api/pipes/standup-update/run?d=2026-04-25"
Rebuild and restart
to pick up the changes:
Copy code to clipboard
cd /volume1/screenpipe/app
docker compose up --build -d
How it works:
Each
pipe.md
file is loaded dynamically — the frontmatter provides metadata and the body becomes the user prompt. Screenpipe data (app usage, browser URLs, OCR, audio) is injected as a Claude system message so the pipe prompt can reference it naturally. The "Read screenpipe skill first" line is stripped automatically.
The pipes directory inside Docker is
/data/pipes
(since
/volume1/screenpipe
→
/data
). You can override it with a
PIPES_DIR
env var if needed.
Message actions
pipe.md
pipe.md
where to place api key and can I use different keys
Thinking
Thinking
Bash
Read .env file
IN
cat /volume1/screenpipe/app/.env
Copy code to clipboard
OUT
[ENV_SECRET]...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21081
|
|
21082
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21082
|
|
21083
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21083
|
|
21094
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp-zshDOCKER* *81DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883-zshPoetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ cat ~/.screenpipe/screenpipe_sync.sh | pbcopylukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~$-zsh*5screenpipe"100% <47*8•Mon 11 May 20:12:01T81• ÷6-zshX7...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21094
|
|
21095
|
screenpipe sync.sh — screenpipe SSH: nas100% 12P• screenpipe sync.sh — screenpipe SSH: nas100% 12P• мon 11 May 20.12-0108000SioV SCREENPIPE ISSH: NASI> #recycleconsumers> static• .env& cli.py# db.pv•docker-compose.vmli# Dockertilemain.py=requirements.txt→data>logs→oinec• aitianore* aoo settinas.ison=archive.db=archive.db-bal= db salite= dh calite.chmE dh calite-wall$ screennine svnc undated shi$ screenpipe sync.snE screennine dhs apo settinas.isonNUANANN47$ screenpipe sync.sh X•pipe.md• .env* Syncs yesterday (derault)sh 2026-04-15screenpipe sync.sh today# syncs today so farat 3am0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh > /Users/lukas/.screenpipe/sync.log 2>&1-euo pipefailCONFIGDB SRC="S(SCREENPIPE DB:-SHOME/.screenpipe/db.salite}"NAS MOUNIE"SINAS MOUNT:- Volumes Test//screendioe?"NAS DB="<NAS MOUNT/archiive.db"NAS DATA-"CNAS MOUNT /data"LOG FILE="$HOME/.screenpipe/sync.log'SCRIPT_START=$(date +%)local msg="[$(date '+%Y-%m-%d %H:%M:&S')] $**ecno suse" ceend SLus relestep)local now=S(date +%s)]loca elaosedes now - SCRIPT START))Ilocal min=s( elansed ( 60)local sec=s( elansed % 60))printf "\n[+%02dm%02ds] • %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"run salite heredoc() {local startes date +%slprintf"9-26c " "clahel"sqLites"SDB SRC" <<< "$sql" &[PASSWORD_DOTS]g.•g.р*)PROBLEMSOUTPUTAdm1nanypAg00pi lIC_R55g: /volume1/ccreenninee* MI23 Implement and run pipe l..Implement and run oipe logleAamin@DXP4800PLUS-B5F8:/volumel//screenpipe/apps sudo docker exec -it screenpipe-app python cli.py pipe day-recap -=date 2026-04-20Running pipe 'day-recap' for 2026-04-20...• Thinking >• The container started before you updated the .env, so it's using the old placeholder key. Restart it to pick up the new one:ca /vo lumel/screenpipe/appsudo docker compose restart screenoine-aooThen try adain:sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20Adm1n@DXP4800PLUS-B5F8:/volume1/screenpipe/app$ sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20Runnina nine 'dav-recan' for 2026-04.20..key is there now but Admin@DXP4800PLUS-B5F8:/volume1/screenpipe/app$ sudo docker exec -it screenpipe-app python cli.py pipe day-recap --date 2026-04-20Runnina oioe 'dav-recao' for 2026-04-20.1• The kev works — it authenticated successfullv. The error is iust that vour Anthropic account has no credits. Go to console.anthropic.com → Plans & Billina and add some credits (a few dollars is enouah fotesting)Once topped up, rerun the same command and it should work.* Esc to focus or unfocus ClaudeM screenpipe_sync.sh€Ask before edit.bash +vO@•|sxOUTIING> TIMELINE*SSH: nas 8 master* ©@0A0 @10 Lukas Kovalik (2 weeks ago) Ln 26, Col 1 Spaces: 2 UTF-8 LF () Shell Script 8 Sign In...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21095
|
|
21096
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 40, Col 23
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.parti...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21096
|
|
21097
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 39, Col 1
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = line.partit...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21097
|
|
21098
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 39, Col 1
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
Info: Setting up SSH Host nas: Setting up SSH tunnel
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range....
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21098
|
|
21099
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 352, Col 47 (13096 selected)
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
Info: Setting up SSH Host nas: Setting up SSH tunnel...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21099
|
|
21100
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 29, Col 35
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
No results found
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21100
|
|
21101
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21101
|
|
21102
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 29, Col 35
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
No results found
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21102
|
|
21103
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
#!/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 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/Test/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables, indexes, FTS"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks
SELECT * FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames
SELECT * FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text
SELECT o.* FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events
SELECT * FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements
SELECT e.* FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings
SELECT * FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: Inline Suggestions, next: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 352, Col 47 (13096 selected)
git-commit Lukas Kovalik (2 weeks ago)
Lukas Kovalik (2 weeks ago)
No results found
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
Thinking
Thinking
Bash
Read summary consumer
IN
ls /volume1/screenpipe/app/consumers/ && cat /volume1/screenpipe/app/consumers/summary.py
Copy code to clipboard
OUT
activity.py
audio.py
elements.py
__init__.py
interactions.py
ocr.py
__pycache__
report.py
summary.py
"""AI summary generator using Claude."""
import os
from datetime import date
from typing import Any
from consumers.activity import app_time, summary_stats, browser_urls, ui_event_summary
from consumers.ocr import deduplicated_text
from consumers.audio import formatted_transcript
SUMMARY_TYPES = {
"day_recap": {
"title": "Day Recap",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a concise day recap using EXACTLY this format:
## Summary
One sentence: what I mainly worked on today.
## Accomplishments
- Top 3 things I finished, with approximate times
- Name specific apps, files, or projects
## Key Moments
- Important things seen or heard, with timestamps
## Unfinished Work
- What to continue tomorrow
## Patterns
- Apps used most, topics that came up repeatedly
Only report what you can verify from the data. End with:
**Next step:** [most important thing to continue]""",
},
"standup": {
"title": "Standup Update",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
## Audio/speech:
{audio}
Write a brief standup update:
**Yesterday:** What I worked on (2-3 bullets)
**Today:** What I plan to continue (1-2 bullets)
**Blockers:** Any issues observed (or "None")
Keep it under 150 words. Only report what's verifiable from the data.""",
},
"focus_time": {
"title": "Focus & Distraction Analysis",
"prompt": """\
You are analyzing a person's computer activity for the day.
## App usage (minutes per app):
{app_time}
## Browser URLs visited:
{browser_urls}
## UI activity per app (clicks / keystrokes / app-switches):
{ui_events}
## Screen content (sampled every 5 minutes):
{ocr_text}
Analyze focus vs distraction patterns:
## Deep Work Periods
- When and how long were sustained focus sessions?
- What was being worked on?
## Distractions
- Which apps/sites were used recreationally?
- How much time was lost to context switching?
## Recommendations
- 2-3 concrete suggestions to improve focus tomorrow
Be specific with times and app names.""",
},
}
def _build_context(for_date: date) -> dict[str, str]:
apps = app_time(for_date)
app_lines = "\n".join(f"- {a['app_name']}: {a['minutes']} min" for a in apps[:20])
urls = browser_urls(for_date, limit=20)
url_lines = "\n".join(
f"- {u['browser_url'][:100]} ({u['minutes']} min)" for u in urls
)
ui_rows = ui_event_summary(for_date)
ui_lines = "\n".join(
f"- {r['app_name']}: {r['clicks']} clicks, {r['keystrokes']} keys, {r['switches']} switches"
for r in ui_rows[:15]
)
ocr_rows = deduplicated_text(for_date, chunk_minutes=5)
ocr_lines = "\n".join(
f"[{r['bucket']}] {r['app_name']} / {r['window_name'] or ''}: {r['text'][:200]}"
for r in ocr_rows[:100]
)
audio = formatted_transcript(for_date)
return {
"app_time": app_lines or "(no app data)",
"browser_urls": url_lines or "(no browser data)",
"ui_events": ui_lines or "(no UI event data)",
"ocr_text": ocr_lines or "(no screen data)",
"audio": audio[:3000] if len(audio) > 3000 else audio,
}
def generate(
summary_type: str = "day_recap",
for_date: date | None = None,
api_key: str | None = None,
) -> dict[str, Any]:
"""Generate an AI summary. Returns dict with 'title', 'content', 'date'."""
import anthropic
d = for_date or date.today()
key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
if not key:
return {
"title": "Error",
"content": "ANTHROPIC_API_KEY not set. Add it to your .env file.",
"date": str(d),
}
spec = SUMMARY_TYPES.get(summary_type)
if not spec:
return {"title": "Error", "content": f"Unknown summary type: {summary_type}", "date": str(d)}
stats = summary_stats(d)
if stats["total_frames"] == 0:
return {
"title": spec["title"],
"content": f"No screen data found for {d}.",
"date": str(d),
}
ctx = _build_context(d)
prompt = spec["prompt"].format(**ctx)
client = anthropic.Anthropic(api_key=key)
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return {
"title": spec["title"],
"content": message.content[0].text,
"date": str(d),
"stats": stats,
}
Thinking
Thinking
Bash
Read all pipe.md files
IN
for f in /volume1/screenpipe/pipes/*/pipe.md; do echo "=== $f ==="; cat "$f"; echo; done
Copy code to clipboard
OUT
=== /volume1/screenpipe/pipes/ai-habits/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: AI Habits
description: "How you use AI tools — patterns and insights"
icon: "🤖"
featured: true
---
Search my recordings from the last 24 hours for AI tool usage. Use app_name filter for each tool separately: ChatGPT, Claude, Copilot, Cursor, Gemini, Perplexity. Use limit=5 per search, max 6 searches total.
Read screenpipe skill first.
Use this exact format:
## AI Tools Used
- List each tool with approximate time spent (e.g. "Claude: ~45min")
## What I Used Them For
- For each tool: coding, writing, research, or brainstorming
## Usage Patterns
- Do I switch between tools? Use them in bursts or steadily?
## Effectiveness
- Which tool appeared alongside completed work vs. abandoned attempts
If no AI usage is found, say so clearly. End with: "**Tip:** [one suggestion to use AI tools more effectively]"
=== /volume1/screenpipe/pipes/day-recap/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only report what you can verify from the data. End with: "**Next step:** [most important thing to continue]"
=== /volume1/screenpipe/pipes/meeting-summary/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Meeting Summary
description: "Summarize meeting transcript with key takeaways and action items"
icon: "🤝"
featured: false
---
Summarize the meeting transcript provided in the context. Include key takeaways and action items. If the meeting is marked as ongoing, note that and summarize what's available so far.
Read screenpipe skill first.
Use this exact format:
## Meeting Summary
One sentence: what this meeting was about.
## Key Takeaways
- Top 3-5 important points discussed
- Include who said what when relevant
## Action Items
- [ ] Task — assigned to whom, deadline if mentioned
- [ ] Task — assigned to whom
## Decisions Made
- List any decisions or agreements reached
## Open Questions
- Anything unresolved or needing follow-up
Keep it concise and actionable.
=== /volume1/screenpipe/pipes/standup-update/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Standup Update
description: "What you did, what's next, and any blockers"
icon: "🏢"
featured: true
---
Based on my recordings from the last 24 hours, generate a standup update. Use limit=10 per search, max 3 searches total.
Read screenpipe skill first.
Use this exact format:
## Yesterday
- What I worked on (name specific projects, files, tools, PRs)
## Today
- What I will work on next (based on unfinished tasks and recent activity)
## Blockers
- Issues I hit — errors, slow builds, waiting on someone
- If no blockers, write "None"
Keep it under 150 words. Copy-paste ready for a team standup.
=== /volume1/screenpipe/pipes/time-breakdown/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Time Breakdown
description: "Where your time went — by app, project, and category"
icon: "⏱"
featured: false
---
Analyze my app usage from today (last 12 hours). Use limit=10 per search, max 4 searches. Prefer /raw_sql with COUNT/GROUP BY queries.
Read screenpipe skill first.
Use this exact format with durations and percentages:
## By Application
- List each app with duration and percentage, sorted by time (e.g. "VS Code: 2h 15min (28%)")
## By Category
- Group into: coding, meetings, browsing, writing, communication, other
- Show hours and percentage per category
## By Project
- Group related activities by project/topic. Name specific repos or tasks.
## Productivity Score
- Calculate: focused_work_hours / total_hours as a percentage
- Focused = coding + writing. Unfocused = browsing + switching.
End with: "**Suggestion:** [one specific change to improve tomorrow's productivity]"
=== /volume1/screenpipe/pipes/video-export/pipe.md ===
---
schedule: manual
enabled: true
template: true
title: Export Video Clip
description: "Create a video of your recent screen activity"
icon: "🎬"
featured: false
---
Export a video of my screen activity from the last 5 minutes.
Read screenpipe skill first.
Use the POST /frames/export endpoint with the time range and fps=1.0. Then show me the exported video file path as an inline code block so I can watch it.
If the export is large, suggest a lower fps or shorter time range.
Thinking
Thinking
Now I have a clear picture. The pipes are markdown prompt templates in
/volume1/screenpipe/pipes/
, but there's no code to actually load and run them —
consumers/summary.py
has its own hardcoded prompts instead.
Here's what I'll implement:
consumers/pipes.py
— dynamically loads pipe.md files and runs them via Claude (data injected as system message, pipe body as user prompt)
main.py
— two new endpoints:
GET /api/pipes
and
POST /api/pipes/{pipe_id}/run
cli.py
— a
pipe
command to run pipes from the terminal
Let me build it:
Bash
Check browser_urls signature
IN
cat /volume1/screenpipe/app/consumers/activity.py | grep "def browser_urls" -A 5
Copy code to clipboard
OUT
def browser_urls(
for_date: date | None = None,
app_name: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
limit: int = 30,
Write
pipes.py
pipes.py
143 lines
"""Dynamic pipe loader and runner."""
import os
import re
from datetime import date
from pathlib import Path
from typing import Any
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
try:
end = text.index("---", 3)
except ValueError:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 3:].strip()
meta: dict = {}
for line in fm_text.splitlines():
if ":" in line:
k, _, v = li...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21103
|
|
21104
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: Inline Suggestions, next: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 480, Col 3
git-commit Not Committed Yet
Not Committed Yet
No results found
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21104
|
|
21105
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
Shell Script
Editor Language Status: Inline Suggestions, next: $(copilot) No inline suggestion available, Inline suggestions
LF
UTF-8
Spaces: 2
Ln 480, Col 3
git-commit Not Committed Yet...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21105
|
|
21106
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21106
|
|
21107
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21107
|
|
21108
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21108
|
|
21109
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm...
|
Code
|
screenpipe_sync.sh — screenpipe [SSH: nas]
|
NULL
|
21109
|
|
21110
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py
M
db.py
docker-compose.yml
Dockerfile
M
main.py
M
requirements.txt
M
data
logs
pipes
.gitignore
app_settings.json
archive.db
archive.db-bak
U
db.sqlite
db.sqlite-shm
db.sqlite-wal
screenpipe_sync_updated.sh
U
screenpipe_sync.sh
screenpipe.db
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
app_settings.json, Editor Group 1
screenpipe_sync.sh, Editor Group 1
pipe.md, Editor Group 1
.env, Editor Group 1
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
#!/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.
#
# Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB,
# the NAS archive gets ALTER TABLE'd to match. Inserts use explicit column lists,
# so positional mismatches can't occur.
#
# Usage:
# ./screenpipe_sync.sh # syncs yesterday (default)
# ./screenpipe_sync.sh 2026-04-15 # syncs a specific date
# ./screenpipe_sync.sh today # syncs today so far
#
# Cron example (runs at 3am daily):
# 0 3 * * * /Users/lukas/.screenpipe/screenpipe_sync.sh >> /Users/lukas/.screenpipe/sync.log 2>&1
set -euo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
DB_SRC="${SCREENPIPE_DB:-$HOME/.screenpipe/db.sqlite}"
NAS_MOUNT="${NAS_MOUNT:-/Volumes/screenpipe}"
NAS_DB="$NAS_MOUNT/archive.db"
NAS_DATA="$NAS_MOUNT/data"
LOG_FILE="$HOME/.screenpipe/sync.log"
# Tables that get schema drift handling. Order matters for FK-ish references
# (parents before children: video_chunks → frames → elements/ocr_text/ui_events).
SYNC_TABLES=(video_chunks frames elements ocr_text ui_events meetings)
# ──────────────────────────────────────────────────────────────────────────────
# ─── HELPERS ──────────────────────────────────────────────────────────────────
SCRIPT_START=$(date +%s)
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
step() {
local now=$(date +%s)
local elapsed=$(( now - SCRIPT_START ))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed % 60 ))
printf "\n[+%02dm%02ds] ▶ %s\n" "$min" "$sec" "$*" | tee -a "$LOG_FILE"
}
run_sqlite_heredoc() {
local label="$1"
local sql="$2"
local start=$(date +%s)
printf " %-36s " "$label"
sqlite3 "$DB_SRC" <<< "$sql" &
local pid=$!
local spin=[PASSWORD] '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %-36s %s " "$label" "${spin[$i]}"
i=$(( (i + 1) % 10 ))
sleep 0.2
done
wait "$pid"
local rc=$?
if [ $rc -ne 0 ]; then
printf "\r %-36s ✗ FAILED\n" "$label" | tee -a "$LOG_FILE"
exit $rc
fi
local dur=$(( $(date +%s) - start ))
printf "\r %-36s ✓ %dm%02ds\n" "$label" "$(( dur / 60 ))" "$(( dur % 60 ))" | tee -a "$LOG_FILE"
}
check() {
local label="$1" got="$2" expected="$3"
if [ "$got" -eq "$expected" ]; then
printf " %-20s %s / %s ✓\n" "$label:" "$got" "$expected"
else
printf " %-20s %s / %s ✗ MISMATCH\n" "$label:" "$got" "$expected"
fi
}
# ─── SCHEMA HELPERS ───────────────────────────────────────────────────────────
# Echoes "name|type" lines for a table from a given DB.
table_columns_with_types() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2 "|" $3}'
}
# Echoes just column names, one per line.
table_columns() {
local db="$1" table="$2"
sqlite3 "$db" "PRAGMA table_info($table);" | awk -F'|' '{print $2}'
}
# Returns true (0) if a table exists in the given DB.
table_exists() {
local db="$1" table="$2"
local count
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$table';")
[ "$count" -gt 0 ]
}
# Adds any columns present in source but missing in NAS for the given table.
# Uses ALTER TABLE ADD COLUMN, which is a fast metadata-only operation in SQLite.
ensure_columns() {
local table="$1"
local label="schema: $table"
printf " %-36s " "$label"
if ! table_exists "$DB_SRC" "$table"; then
printf "✗ source missing — skipping\n"
return 0
fi
if ! table_exists "$NAS_DB" "$table"; then
printf "✓ fresh (created above)\n"
return 0
fi
local src_cols
src_cols=$(table_columns_with_types "$DB_SRC" "$table")
local nas_cols
nas_cols=$(table_columns "$NAS_DB" "$table")
local added=0
local added_names=""
while IFS='|' read -r name type; do
[ -z "$name" ] && continue
# Use grep -F -x for fixed-string exact match (safe against regex chars).
if ! echo "$nas_cols" | grep -Fxq "$name"; then
# SQLite ALTER TABLE ADD COLUMN: type is optional, no NOT NULL allowed
# without a constant default. Default-NULL is exactly what we want for archive.
sqlite3 "$NAS_DB" "ALTER TABLE $table ADD COLUMN \"$name\" $type;"
added=$((added + 1))
added_names="$added_names $name"
fi
done <<< "$src_cols"
if [ "$added" -gt 0 ]; then
printf "✓ added %d:%s\n" "$added" "$added_names"
else
printf "✓ in sync\n"
fi
}
# Echoes a comma-separated list of source columns for a table, double-quoted
# so reserved words and hyphens are safe. If an alias is provided, columns are
# prefixed (e.g. `o."col"`). Used for both INSERT and SELECT sides; aliased
# form is required when SELECTing from a JOIN where column names collide
# (e.g. ocr_text and frames both have app_name, window_name).
build_col_list() {
local table="$1"
local alias="${2:-}"
local prefix=""
[ -n "$alias" ] && prefix="${alias}."
table_columns "$DB_SRC" "$table" | awk 'NF' | awk -v p="$prefix" '{print p "\"" $0 "\""}' | paste -sd, -
}
# ──────────────────────────────────────────────────────────────────────────────
# ─── DATE ARGUMENT ────────────────────────────────────────────────────────────
if [ "${1:-}" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
elif [ -n "${1:-}" ]; then
TARGET_DATE="$1"
if ! [[ "$TARGET_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "ERROR: Invalid date format. Use YYYY-MM-DD, 'today', or no argument for yesterday."
exit 1
fi
else
TARGET_DATE=$(date -v-1d +%Y-%m-%d)
fi
log "========================================"
log "Screenpipe sync starting for: $TARGET_DATE"
log "========================================"
# ─── PREFLIGHT ────────────────────────────────────────────────────────────────
step "Preflight checks"
if [ ! -f "$DB_SRC" ]; then
log "ERROR: Source DB not found at $DB_SRC"; exit 1
fi
printf " %-20s %s (%s)\n" "Source DB:" "OK" "$(du -sh "$DB_SRC" | cut -f1)"
if [ ! -d "$NAS_MOUNT" ]; then
log "ERROR: NAS not mounted at $NAS_MOUNT"; exit 1
fi
printf " %-20s %s\n" "NAS mount:" "OK $NAS_MOUNT"
# Check if DB already synced for this date
DB_ALREADY_SYNCED=false
if [ -f "$NAS_DB" ]; then
if table_exists "$NAS_DB" "frames"; then
EXISTING=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';" 2>/dev/null || echo "0")
if [ "$EXISTING" -gt "0" ]; then
log "Date $TARGET_DATE already has $EXISTING frames in archive — skipping DB sync"
DB_ALREADY_SYNCED=true
else
printf " %-20s %s (%s)\n" "Archive DB:" "exists" "$(du -sh "$NAS_DB" | cut -f1)"
fi
else
printf " %-20s %s\n" "Archive DB:" "exists, no frames table yet"
fi
else
printf " %-20s %s\n" "Archive DB:" "will be created"
fi
# Source data dir for this date
DATA_SRC="$HOME/.screenpipe/data/data/$TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
DATA_SIZE=$(du -sh "$DATA_SRC" | cut -f1)
DATA_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
printf " %-20s %s (%s files, %s)\n" "Data dir:" "OK" "$DATA_FILES" "$DATA_SIZE"
else
printf " %-20s %s\n" "Data dir:" "not found — skipping file copy"
fi
# ─── DB SYNC ──────────────────────────────────────────────────────────────────
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── COUNT SOURCE ROWS ────────────────────────────────────────────────────
step "Counting source rows for $TARGET_DATE"
SRC_FRAMES=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
SRC_ELEMENTS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_UI=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
SRC_OCR=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
SRC_MEETINGS=$(sqlite3 "$DB_SRC" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start) = '$TARGET_DATE';")
printf " %-20s %s\n" "frames:" "$SRC_FRAMES"
printf " %-20s %s\n" "elements:" "$SRC_ELEMENTS"
printf " %-20s %s\n" "ui_events:" "$SRC_UI"
printf " %-20s %s\n" "ocr_text:" "$SRC_OCR"
printf " %-20s %s\n" "meetings:" "$SRC_MEETINGS"
if [ "$SRC_FRAMES" -eq "0" ]; then
log "No frames found for $TARGET_DATE — skipping DB sync"
DB_ALREADY_SYNCED=true
fi
fi
if [ "$DB_ALREADY_SYNCED" = false ]; then
# ─── INIT TABLES ──────────────────────────────────────────────────────────
step "Initialising tables (CREATE IF NOT EXISTS)"
run_sqlite_heredoc "creating tables" "
ATTACH '$NAS_DB' AS nas;
CREATE TABLE IF NOT EXISTS nas.frames AS SELECT * FROM main.frames WHERE 0;
CREATE TABLE IF NOT EXISTS nas.elements AS SELECT * FROM main.elements WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ui_events AS SELECT * FROM main.ui_events WHERE 0;
CREATE TABLE IF NOT EXISTS nas.ocr_text AS SELECT * FROM main.ocr_text WHERE 0;
CREATE TABLE IF NOT EXISTS nas.video_chunks AS SELECT * FROM main.video_chunks WHERE 0;
CREATE TABLE IF NOT EXISTS nas.meetings AS SELECT * FROM main.meetings WHERE 0;
DETACH nas;
"
# ─── SCHEMA DRIFT FIX ─────────────────────────────────────────────────────
# Runs AFTER CREATE IF NOT EXISTS so newly-created tables are no-ops here,
# and pre-existing tables get any missing columns added.
step "Reconciling NAS schema with source"
for tbl in "${SYNC_TABLES[@]}"; do
ensure_columns "$tbl"
done
run_sqlite_heredoc "creating indexes" "
ATTACH '$NAS_DB' AS nas;
CREATE INDEX IF NOT EXISTS nas.idx_frames_timestamp ON frames(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_frames_app_name ON frames(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_window_name ON frames(window_name);
CREATE INDEX IF NOT EXISTS nas.idx_frames_video_chunk_id ON frames(video_chunk_id);
CREATE INDEX IF NOT EXISTS nas.idx_frames_document_path ON frames(document_path) WHERE document_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_id ON elements(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_elements_frame_src_role ON elements(frame_id, source, role) WHERE text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_elements_onscreen_frame ON elements(frame_id) WHERE on_screen = 1 AND text IS NOT NULL;
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_timestamp ON ui_events(timestamp);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_app_name ON ui_events(app_name);
CREATE INDEX IF NOT EXISTS nas.idx_ui_events_frame_id ON ui_events(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_ocr_text_frame_id ON ocr_text(frame_id);
CREATE INDEX IF NOT EXISTS nas.idx_meetings_start ON meetings(meeting_start);
CREATE INDEX IF NOT EXISTS nas.idx_video_chunks_device ON video_chunks(device_name);
DETACH nas;
"
run_sqlite_heredoc "creating FTS tables" "
ATTACH '$NAS_DB' AS nas;
CREATE VIRTUAL TABLE IF NOT EXISTS nas.elements_fts USING fts5(
text, role, frame_id UNINDEXED,
content='elements', content_rowid='id', tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.frames_fts USING fts5(
full_text, app_name, window_name, browser_url, id UNINDEXED,
tokenize='unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS nas.ui_events_fts USING fts5(
text_content,
app_name,
window_title,
element_name,
content='ui_events',
content_rowid='id',
tokenize='unicode61'
);
DETACH nas;
"
# ─── BUILD EXPLICIT COLUMN LISTS ──────────────────────────────────────────
# After ensure_columns, source ⊆ NAS for every synced table, so source's
# column list is a safe subset to use on both sides of the INSERT.
# For tables that JOIN frames (ocr_text, elements), the SELECT side needs
# alias-qualified columns because both tables share names (app_name, etc).
FRAMES_COLS=$(build_col_list frames)
ELEMENTS_COLS=$(build_col_list elements)
ELEMENTS_COLS_E=$(build_col_list elements e)
UI_EVENTS_COLS=$(build_col_list ui_events)
OCR_TEXT_COLS=$(build_col_list ocr_text)
OCR_TEXT_COLS_O=$(build_col_list ocr_text o)
VIDEO_CHUNKS_COLS=$(build_col_list video_chunks)
MEETINGS_COLS=$(build_col_list meetings)
# ─── SYNC DATA ────────────────────────────────────────────────────────────
step "Syncing data for $TARGET_DATE"
run_sqlite_heredoc "video_chunks" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.video_chunks ($VIDEO_CHUNKS_COLS)
SELECT $VIDEO_CHUNKS_COLS FROM main.video_chunks
WHERE id IN (
SELECT DISTINCT video_chunk_id FROM main.frames
WHERE date(timestamp) = '$TARGET_DATE' AND video_chunk_id IS NOT NULL
);
DETACH nas;
"
run_sqlite_heredoc "frames ($SRC_FRAMES rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.frames ($FRAMES_COLS)
SELECT $FRAMES_COLS FROM main.frames WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ocr_text ($SRC_OCR rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ocr_text ($OCR_TEXT_COLS)
SELECT $OCR_TEXT_COLS_O FROM main.ocr_text o
JOIN main.frames f ON o.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "ui_events ($SRC_UI rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.ui_events ($UI_EVENTS_COLS)
SELECT $UI_EVENTS_COLS FROM main.ui_events WHERE date(timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "elements ($SRC_ELEMENTS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.elements ($ELEMENTS_COLS)
SELECT $ELEMENTS_COLS_E FROM main.elements e
JOIN main.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE';
DETACH nas;
"
run_sqlite_heredoc "meetings ($SRC_MEETINGS rows)" "
ATTACH '$NAS_DB' AS nas;
INSERT OR IGNORE INTO nas.meetings ($MEETINGS_COLS)
SELECT $MEETINGS_COLS FROM main.meetings WHERE date(meeting_start) = '$TARGET_DATE';
DETACH nas;
"
# ─── FTS UPDATE ───────────────────────────────────────────────────────────
step "Updating FTS indexes"
run_sqlite_heredoc "elements_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.elements_fts(rowid, text, role)
SELECT e.id, e.text, e.role
FROM nas.elements e
JOIN nas.frames f ON e.frame_id = f.id
WHERE date(f.timestamp) = '$TARGET_DATE'
AND e.text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "frames_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.frames_fts(rowid, full_text, app_name, window_name, browser_url, id)
SELECT id, full_text, app_name, window_name, browser_url, id
FROM nas.frames
WHERE date(timestamp) = '$TARGET_DATE'
AND full_text IS NOT NULL;
DETACH nas;
"
run_sqlite_heredoc "ui_events_fts" "
ATTACH '$NAS_DB' AS nas;
INSERT INTO nas.ui_events_fts(rowid, text_content, app_name, window_title, element_name)
SELECT id, text_content, app_name, window_title, element_name
FROM nas.ui_events
WHERE date(timestamp) = '$TARGET_DATE'
AND text_content IS NOT NULL;
DETACH nas;
"
# ─── VERIFY DB ────────────────────────────────────────────────────────────
step "Verifying DB"
V_FRAMES=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM frames WHERE date(timestamp) = '$TARGET_DATE';")
V_ELEMENTS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM elements WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_UI=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ui_events WHERE date(timestamp) = '$TARGET_DATE';")
V_OCR=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM ocr_text WHERE frame_id IN (SELECT id FROM frames WHERE date(timestamp) = '$TARGET_DATE');")
V_MEETINGS=$(sqlite3 "$NAS_DB" "SELECT COUNT(*) FROM meetings WHERE date(meeting_start)= '$TARGET_DATE';")
check "frames" "$V_FRAMES" "$SRC_FRAMES"
check "elements" "$V_ELEMENTS" "$SRC_ELEMENTS"
check "ui_events" "$V_UI" "$SRC_UI"
check "ocr_text" "$V_OCR" "$SRC_OCR"
check "meetings" "$V_MEETINGS" "$SRC_MEETINGS"
fi
# ─── COPY DATA FOLDER ─────────────────────────────────────────────────────────
# Always runs regardless of DB sync status
step "Copying data folder for $TARGET_DATE"
if [ -d "$DATA_SRC" ]; then
mkdir -p "$NAS_DATA/$TARGET_DATE"
RSYNC_START=$(date +%s)
printf " %-36s " "rsync $TARGET_DATE/ → NAS"
rsync -a --ignore-existing \
"$DATA_SRC/" \
"$NAS_DATA/$TARGET_DATE/" \
2>>"$LOG_FILE"
RSYNC_DUR=$(( $(date +%s) - RSYNC_START ))
COPIED_FILES=$(ls "$NAS_DATA/$TARGET_DATE" | wc -l | tr -d ' ')
SRC_FILES=$(ls "$DATA_SRC" | wc -l | tr -d ' ')
COPIED_SIZE=$(du -sh "$NAS_DATA/$TARGET_DATE" | cut -f1)
if [ "$COPIED_FILES" -eq "$SRC_FILES" ]; then
printf "\r %-36s ✓ %dm%02ds (%s files, %s)\n" \
"rsync $TARGET_DATE/ → NAS" \
"$(( RSYNC_DUR / 60 ))" "$(( RSYNC_DUR % 60 ))" \
"$COPIED_FILES" "$COPIED_SIZE" | tee -a "$LOG_FILE"
else
printf "\r %-36s ✗ %s / %s files\n" \
"rsync $TARGET_DATE/ → NAS" "$COPIED_FILES" "$SRC_FILES" | tee -a "$LOG_FILE"
fi
else
printf " %-36s %s\n" "rsync $TARGET_DATE/ → NAS" "skipped (no source dir)"
fi
# ─── COPY LOGS ────────────────────────────────────────────────────────────────
# Screenpipe writes daily logs to ~/.screenpipe/screenpipe.YYYY-MM-DD.N.log
# (N is a rotation counter, almost always 0). Mirror them to NAS so log
# analysis can happen on the consumer side without SSH'ing back to the Mac.
step "Copying screenpipe logs for $TARGET_DATE"
NAS_LOGS="$NAS_MOUNT/logs"
mkdir -p "$NAS_LOGS"
# Use a glob with nullglob so an empty match doesn't expand to a literal pattern.
shopt -s nullglob
LOG_FILES=( "$HOME/.screenpipe/screenpipe.$TARGET_DATE."*.log )
shopt -u nullglob
if [ ${#LOG_FILES[@]} -gt 0 ]; then
printf " %-36s " "rsync logs → NAS"
# No --ignore-existing here: logs grow during the day, so we want rsync's
# default size+mtime check to refresh files that have changed.
rsync -a "${LOG_FILES[@]}" "$NAS_LOGS/" 2>>"$LOG_FILE"
COPIED=${#LOG_FILES[@]}
TOTAL_SIZE=$(du -ch "${LOG_FILES[@]}" | tail -1 | cut -f1)
printf "✓ %d file(s), %s\n" "$COPIED" "$TOTAL_SIZE" | tee -a "$LOG_FILE"
else
printf " %-36s %s\n" "rsync logs → NAS" "skipped (no matching logs)"
fi
# ─── SUMMARY ──────────────────────────────────────────────────────────────────
TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START ))
DB_SIZE=$(du -sh "$NAS_DB" | cut -f1)
echo ""
log "Archive DB size: $DB_SIZE"
log "Total time: $(( TOTAL_ELAPSED / 60 ))m$(( TOTAL_ELAPSED % 60 ))s"
log "Sync complete for $TARGET_DATE"
log "========================================"
# Mirror this script's own logbook to NAS so sync health can be monitored
# from the consumer side. Done last so the summary lines above are included.
rsync -a "$HOME/.screenpipe/sync.log" "$NAS_LOGS/sync.log" 2>/dev/null || true
Implement and run pipe l…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports - 1 forwarded port
PORTS
1
remote SSH: nas
SSH: nas
screenpipe (Git) - master*, Checkout Branch/Tag...
master*
screenpipe (Git) - Synchronize Changes
No Problems
0
0
Forwarded Ports: 8766
1
Notifications
Sign In
Sign In
No results found
Implement and run pipe logic
Session history
New session
Message actions
main.py
main.py
lets implement logic with pipes. how can I run it?
Thinking
Thinking
Let me look at the current project structure to understand what's already in place.
Bash
List project files
IN
find /volume1/screenpipe -type f | head -50 && echo "---" && ls /volume1/screenpipe
Copy code to clipboard
OUT
/volume1/screenpipe/.DS_Store
/volume1/screenpipe/screenpipe.db
/volume1/screenpipe/screenpipe_sync.sh
/volume1/screenpipe/app_settings.json
/volume1/screenpipe/.gitignore
/volume1/screenpipe/archive.db
/volume1/screenpipe/db.sqlite
/volume1/screenpipe/db.sqlite-wal
/volume1/screenpipe/db.sqlite-shm
/volume1/screenpipe/#recycle/desktop.ini
/volume1/screenpipe/#recycle/db.sqlite
/volume1/screenpipe/#recycle/Copy #1 of db.sqlite
/volume1/screenpipe/#recycle/.DS_Store
/volume1/screenpipe/#recycle/Copy #2 of db.sqlite
/volume1/screenpipe/#recycle/archive.db
/volume1/screenpipe/#recycle/Copy #1 of archive.db
/volume1/screenpipe/#recycle/Copy #2 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of archive.db
/volume1/screenpipe/#recycle/Copy #4 of archive.db
/volume1/screenpipe/#recycle/Copy #5 of archive.db
/volume1/screenpipe/#recycle/Copy #3 of db.sqlite
/volume1/screenpipe/#recycle/Copy #4 of db.sqlite
/volume1/screenpipe/#recycle/Copy #6 of archive.db
/volume1/screenpipe/#recycle/test.txt
/volume1/screenpipe/#recycle/Copy #7 of archive.db
/volume1/screenpipe/#recycle/Copy #8 of archive.db
/volume1/screenpipe/#recycle/archive.db-journal
/volume1/screenpipe/#recycle/Copy #1 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #2 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #3 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #4 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #6 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #7 of archive.db-journal
/volume1/screenpipe/#recycle/Copy #5 of db.sqlite
/volume1/screenpipe/pipes/.DS_Store
/volume1/screenpipe/app/.env
/volume1/screenpipe/app/cli.py
/volume1/screenpipe/app/.DS_Store
/volume1/screenpipe/app/db.py
/volume1/screenpipe/app/docker-compose.yml
/volume1/screenpipe/app/main.py
/volume1/screenpipe/app/requirements.txt
/volume1/screenpipe/app/Dockerfile
/volume1/screenpipe/data/.DS_Store
/volume1/screenpipe/.git/description
/volume1/screenpipe/.git/HEAD
/volume1/screenpipe/.git/index
/volume1/screenpipe/.git/COMMIT_EDITMSG
/volume1/screenpipe/#recycle/data/.DS_Store
---
app
app_settings.json
archive.db
data
db.sqlite
db.sqlite-shm
db.sqlite-wal
pipes
#recycle
screenpipe.db
screenpipe_sync.sh
Read
main.py
main.py
Bash
Check pipes directory and docker-compose
IN
ls /volume1/screenpipe/pipes/ && ls /volume1/screenpipe/app/ && cat /volume1/screenpipe/app/docker-compose.yml
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
cli.py
consumers
db.py
docker-compose.yml
Dockerfile
main.py
__pycache__
requirements.txt
static
services:
screenpipe-app:
build: .
container_name: screenpipe-app
restart: unless-stopped
ports:
- "8766:8000"
volumes:
- /volume1/screenpipe:/data
env_file:
- .env
environment:
TZ: Europe/Sofia
DB_PATH: /data/db.sqlite
sqlite-web-archive:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-archive
restart: unless-stopped
ports:
- "8767:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/archive.db"]
sqlite-web-live:
image: ghcr.io/coleifer/sqlite-web:latest
container_name: screenpipe-sqlweb-live
restart: unless-stopped
ports:
- "8768:8080"
volumes:
- /volume1/screenpipe:/data
command: ["-H", "[IP_ADDRESS]", "-r", "/data/db.sqlite"]
Read
requirements.txt
requirements.txt
Bash
Inspect a pipe directory
IN
ls /volume1/screenpipe/pipes/day-recap/ && cat /volume1/screenpipe/pipes/day-recap/pipe.json 2>/dev/null || ls /volume1/screenpipe/pipes/day-recap/
Copy code to clipboard
OUT
pipe.md
pipe.md
Read
cli.py
cli.py
Bash
List all pipes and their contents
IN
ls /volume1/screenpipe/pipes/ && for d in /volume1/screenpipe/pipes/*/; do echo "=== $d ==="; ls "$d"; done
Copy code to clipboard
OUT
ai-habits
day-recap
meeting-summary
standup-update
time-breakdown
video-export
=== /volume1/screenpipe/pipes/ai-habits/ ===
pipe.md
=== /volume1/screenpipe/pipes/day-recap/ ===
pipe.md
=== /volume1/screenpipe/pipes/meeting-summary/ ===
pipe.md
=== /volume1/screenpipe/pipes/standup-update/ ===
pipe.md
=== /volume1/screenpipe/pipes/time-breakdown/ ===
pipe.md
=== /volume1/screenpipe/pipes/video-export/ ===
pipe.md
Bash
Read pipe markdown files
IN
cat /volume1/screenpipe/pipes/day-recap/pipe.md && echo "===" && cat /volume1/screenpipe/pipes/meeting-summary/pipe.md 2>/dev/null | head -50
Copy code to clipboard
OUT
---
schedule: manual
enabled: true
template: true
title: Day Recap
description: "Today's accomplishments, key moments, and unfinished work"
icon: "📋"
featured: true
---
Analyze my screen and audio recordings from today (last 16 hours only).
Read screenpipe skill first.
Use this exact format:
## Summary
One sentence: what I mainly did today.
## Accomplishments
- Top 3 things I finished, with timestamps (e.g. "2:30 PM")
- Name specific apps, files, or projects
## Key Moments
- Important things I saw, said, or heard — with timestamps
## Unfinished Work
- What I should continue tomorrow — name the app/file/task
## Patterns
- Apps I used most, topics that came up repeatedly
Only rep...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21110
|
|
21111
|
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Explorer (⇧⌘E) - 1 unsaved file
1
Search (⇧⌘F)
Source Control (⌃⇧G) - 10 pending changes
10
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: screenpipe [SSH: nas]
Explorer Section: screenpipe [SSH: nas]
SCREENPIPE [SSH: NAS]
#recycle
app
__pycache__
consumers
static
.env
cli.py...
|
Code
|
Implement and run pipe l… — screenpipe [SSH: nas]
|
NULL
|
21111
|