screenpipe-sync 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/dist/index.js +261 -10
- package/package.json +1 -1
- package/src/index.ts +310 -9
package/README.md
CHANGED
|
@@ -7,17 +7,17 @@ Turn hours of screen recordings into actionable context: todos, goals, decisions
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# One-liner -
|
|
11
|
-
bunx
|
|
10
|
+
# One-liner - AI summary to stdout
|
|
11
|
+
bunx screenpipe-sync
|
|
12
12
|
|
|
13
|
-
# Save
|
|
14
|
-
bunx
|
|
13
|
+
# Save daily summaries locally
|
|
14
|
+
bunx screenpipe-sync --output ~/Documents/brain/context --git
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
bunx
|
|
16
|
+
# Sync raw SQLite database to remote (full history!)
|
|
17
|
+
bunx screenpipe-sync --db --remote user@host:~/.screenpipe/
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
bunx
|
|
19
|
+
# Full sync: DB + daily summary
|
|
20
|
+
bunx screenpipe-sync --db -r clawdbot:~/.screenpipe && bunx screenpipe-sync -o ~/context -g
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## What It Extracts
|
|
@@ -114,12 +114,23 @@ bunx @screenpipe/sync --hours 168 --json > week.json
|
|
|
114
114
|
|
|
115
115
|
## How It Works
|
|
116
116
|
|
|
117
|
+
### Summary Mode (default)
|
|
117
118
|
1. **Query** - Fetches OCR data from local Screenpipe API
|
|
118
119
|
2. **Dedupe** - Removes duplicate/similar screen captures
|
|
119
120
|
3. **Extract** - Claude analyzes content for structured data
|
|
120
121
|
4. **Format** - Outputs markdown or JSON
|
|
121
122
|
5. **Sync** - Optionally git pushes or SCPs to remote
|
|
122
123
|
|
|
124
|
+
### DB Sync Mode (`--db`)
|
|
125
|
+
1. **Copy** - Copies `~/.screenpipe/db.sqlite` (your full history)
|
|
126
|
+
2. **Sync** - Uses rsync/scp to transfer to remote
|
|
127
|
+
3. **Query** - Remote can query SQLite directly
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# On remote, query your full history:
|
|
131
|
+
sqlite3 ~/.screenpipe/db.sqlite "SELECT text FROM ocr_text WHERE text LIKE '%meeting%' LIMIT 10;"
|
|
132
|
+
```
|
|
133
|
+
|
|
123
134
|
## Requirements
|
|
124
135
|
|
|
125
136
|
- [Screenpipe](https://github.com/mediar-ai/screenpipe) running locally
|
package/dist/index.js
CHANGED
|
@@ -9603,6 +9603,7 @@ var sdk_default = Anthropic;
|
|
|
9603
9603
|
// src/index.ts
|
|
9604
9604
|
function parseArgs() {
|
|
9605
9605
|
const args = process.argv.slice(2);
|
|
9606
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
9606
9607
|
const config = {
|
|
9607
9608
|
screenpipeUrl: process.env.SCREENPIPE_URL || "http://localhost:3030",
|
|
9608
9609
|
outputDir: null,
|
|
@@ -9614,7 +9615,12 @@ function parseArgs() {
|
|
|
9614
9615
|
ollamaUrl: process.env.OLLAMA_URL || "http://localhost:11434",
|
|
9615
9616
|
ollamaModel: process.env.OLLAMA_MODEL || "llama3.2",
|
|
9616
9617
|
format: "markdown",
|
|
9617
|
-
verbose: false
|
|
9618
|
+
verbose: false,
|
|
9619
|
+
dbSync: false,
|
|
9620
|
+
dbPath: process.env.SCREENPIPE_DB || `${home}/.screenpipe/db.sqlite`,
|
|
9621
|
+
daemon: false,
|
|
9622
|
+
daemonInterval: 3600,
|
|
9623
|
+
daemonStop: false
|
|
9618
9624
|
};
|
|
9619
9625
|
for (let i2 = 0;i2 < args.length; i2++) {
|
|
9620
9626
|
const arg = args[i2];
|
|
@@ -9642,6 +9648,24 @@ function parseArgs() {
|
|
|
9642
9648
|
case "-v":
|
|
9643
9649
|
config.verbose = true;
|
|
9644
9650
|
break;
|
|
9651
|
+
case "--db":
|
|
9652
|
+
case "--db-sync":
|
|
9653
|
+
config.dbSync = true;
|
|
9654
|
+
break;
|
|
9655
|
+
case "--db-path":
|
|
9656
|
+
config.dbPath = args[++i2];
|
|
9657
|
+
break;
|
|
9658
|
+
case "--daemon":
|
|
9659
|
+
case "-d":
|
|
9660
|
+
config.daemon = true;
|
|
9661
|
+
config.dbSync = true;
|
|
9662
|
+
break;
|
|
9663
|
+
case "--interval":
|
|
9664
|
+
config.daemonInterval = parseInt(args[++i2]) || 3600;
|
|
9665
|
+
break;
|
|
9666
|
+
case "--stop":
|
|
9667
|
+
config.daemonStop = true;
|
|
9668
|
+
break;
|
|
9645
9669
|
case "--help":
|
|
9646
9670
|
printHelp();
|
|
9647
9671
|
process.exit(0);
|
|
@@ -9651,10 +9675,14 @@ function parseArgs() {
|
|
|
9651
9675
|
}
|
|
9652
9676
|
function printHelp() {
|
|
9653
9677
|
console.log(`
|
|
9654
|
-
|
|
9678
|
+
screenpipe-sync - Extract daily context from Screenpipe
|
|
9655
9679
|
|
|
9656
9680
|
USAGE:
|
|
9657
|
-
bunx
|
|
9681
|
+
bunx screenpipe-sync [options]
|
|
9682
|
+
|
|
9683
|
+
MODES:
|
|
9684
|
+
Summary mode (default): AI-powered daily summary extraction
|
|
9685
|
+
DB sync mode (--db): Copy raw SQLite database to remote
|
|
9658
9686
|
|
|
9659
9687
|
OPTIONS:
|
|
9660
9688
|
-o, --output <dir> Save summary to directory (default: stdout)
|
|
@@ -9664,18 +9692,38 @@ OPTIONS:
|
|
|
9664
9692
|
--json Output as JSON instead of markdown
|
|
9665
9693
|
-v, --verbose Show debug output
|
|
9666
9694
|
|
|
9695
|
+
--db, --db-sync Sync raw SQLite database instead of summary
|
|
9696
|
+
--db-path <path> Path to Screenpipe DB (default: ~/.screenpipe/db.sqlite)
|
|
9697
|
+
|
|
9698
|
+
-d, --daemon Install persistent background sync (survives reboot)
|
|
9699
|
+
--interval <secs> Sync interval in seconds (default: 3600 = 1 hour)
|
|
9700
|
+
--stop Stop and remove the daemon
|
|
9701
|
+
|
|
9667
9702
|
ENVIRONMENT:
|
|
9668
9703
|
SCREENPIPE_URL Screenpipe API URL (default: http://localhost:3030)
|
|
9669
|
-
|
|
9704
|
+
SCREENPIPE_DB Path to Screenpipe database
|
|
9705
|
+
ANTHROPIC_API_KEY For AI summarization (or OPENAI_API_KEY)
|
|
9670
9706
|
|
|
9671
9707
|
EXAMPLES:
|
|
9672
|
-
|
|
9673
|
-
bunx
|
|
9674
|
-
|
|
9675
|
-
|
|
9708
|
+
# AI summary to stdout
|
|
9709
|
+
bunx screenpipe-sync
|
|
9710
|
+
|
|
9711
|
+
# Save daily summaries locally
|
|
9712
|
+
bunx screenpipe-sync --output ~/Documents/brain/context --git
|
|
9676
9713
|
|
|
9677
|
-
|
|
9678
|
-
|
|
9714
|
+
# Sync raw database to remote (e.g., Clawdbot)
|
|
9715
|
+
bunx screenpipe-sync --db --remote user@clawdbot:~/.screenpipe/
|
|
9716
|
+
|
|
9717
|
+
# Full sync: DB + daily summary
|
|
9718
|
+
bunx screenpipe-sync --db -r clawdbot:~/.screenpipe && bunx screenpipe-sync -o ~/context -g
|
|
9719
|
+
|
|
9720
|
+
# ONE-LINER: Permanent background sync (survives reboot)
|
|
9721
|
+
bunx screenpipe-sync --daemon --remote user@server:~/.screenpipe/
|
|
9722
|
+
|
|
9723
|
+
# Stop the daemon
|
|
9724
|
+
bunx screenpipe-sync --stop
|
|
9725
|
+
|
|
9726
|
+
OUTPUT (summary mode):
|
|
9679
9727
|
- Todo items extracted from screen content
|
|
9680
9728
|
- Goals and intentions mentioned
|
|
9681
9729
|
- Decisions made
|
|
@@ -9683,6 +9731,10 @@ OUTPUT:
|
|
|
9683
9731
|
- Meetings and conversations
|
|
9684
9732
|
- Blockers and problems
|
|
9685
9733
|
- AI-generated insights
|
|
9734
|
+
|
|
9735
|
+
OUTPUT (db mode):
|
|
9736
|
+
- Copies ~/.screenpipe/db.sqlite to remote
|
|
9737
|
+
- Remote can query SQLite directly for full history
|
|
9686
9738
|
`);
|
|
9687
9739
|
}
|
|
9688
9740
|
async function queryScreenpipe(config) {
|
|
@@ -9921,9 +9973,208 @@ async function writeOutput(content, config, filename) {
|
|
|
9921
9973
|
}
|
|
9922
9974
|
}
|
|
9923
9975
|
}
|
|
9976
|
+
async function setupDaemon(config) {
|
|
9977
|
+
const fs2 = await import("fs/promises");
|
|
9978
|
+
const { execSync } = await import("child_process");
|
|
9979
|
+
const os = await import("os");
|
|
9980
|
+
const path = await import("path");
|
|
9981
|
+
const home = os.homedir();
|
|
9982
|
+
const platform = os.platform();
|
|
9983
|
+
if (!config.remote && !config.outputDir) {
|
|
9984
|
+
console.error(`[error] --daemon requires --remote or --output`);
|
|
9985
|
+
console.error(` Example: bunx screenpipe-sync --daemon -r user@host:~/.screenpipe/`);
|
|
9986
|
+
process.exit(1);
|
|
9987
|
+
}
|
|
9988
|
+
const remotePart = config.remote ? `--remote ${config.remote}` : "";
|
|
9989
|
+
const outputPart = config.outputDir ? `--output ${config.outputDir}` : "";
|
|
9990
|
+
const gitPart = config.gitPush ? "--git" : "";
|
|
9991
|
+
if (platform === "darwin") {
|
|
9992
|
+
const plistPath = path.join(home, "Library/LaunchAgents/com.screenpipe.sync.plist");
|
|
9993
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
9994
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9995
|
+
<plist version="1.0">
|
|
9996
|
+
<dict>
|
|
9997
|
+
<key>Label</key>
|
|
9998
|
+
<string>com.screenpipe.sync</string>
|
|
9999
|
+
<key>ProgramArguments</key>
|
|
10000
|
+
<array>
|
|
10001
|
+
<string>/bin/bash</string>
|
|
10002
|
+
<string>-c</string>
|
|
10003
|
+
<string>export PATH="$HOME/.bun/bin:/usr/local/bin:/opt/homebrew/bin:$PATH" && bunx screenpipe-sync --db ${remotePart} ${outputPart} ${gitPart}</string>
|
|
10004
|
+
</array>
|
|
10005
|
+
<key>StartInterval</key>
|
|
10006
|
+
<integer>${config.daemonInterval}</integer>
|
|
10007
|
+
<key>RunAtLoad</key>
|
|
10008
|
+
<true/>
|
|
10009
|
+
<key>StandardOutPath</key>
|
|
10010
|
+
<string>/tmp/screenpipe-sync.log</string>
|
|
10011
|
+
<key>StandardErrorPath</key>
|
|
10012
|
+
<string>/tmp/screenpipe-sync.err</string>
|
|
10013
|
+
<key>EnvironmentVariables</key>
|
|
10014
|
+
<dict>
|
|
10015
|
+
<key>HOME</key>
|
|
10016
|
+
<string>${home}</string>
|
|
10017
|
+
</dict>
|
|
10018
|
+
</dict>
|
|
10019
|
+
</plist>`;
|
|
10020
|
+
await fs2.mkdir(path.dirname(plistPath), { recursive: true });
|
|
10021
|
+
await fs2.writeFile(plistPath, plist);
|
|
10022
|
+
try {
|
|
10023
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`);
|
|
10024
|
+
execSync(`launchctl load "${plistPath}"`);
|
|
10025
|
+
} catch (e2) {
|
|
10026
|
+
console.error(`[error] Failed to load LaunchAgent: ${e2}`);
|
|
10027
|
+
process.exit(1);
|
|
10028
|
+
}
|
|
10029
|
+
console.log(`[ok] Daemon installed (macOS LaunchAgent)`);
|
|
10030
|
+
console.log(` Syncs every ${config.daemonInterval}s to: ${config.remote || config.outputDir}`);
|
|
10031
|
+
console.log(` Logs: /tmp/screenpipe-sync.log`);
|
|
10032
|
+
console.log(` Stop: bunx screenpipe-sync --stop`);
|
|
10033
|
+
} else if (platform === "linux") {
|
|
10034
|
+
const serviceDir = path.join(home, ".config/systemd/user");
|
|
10035
|
+
const servicePath = path.join(serviceDir, "screenpipe-sync.service");
|
|
10036
|
+
const timerPath = path.join(serviceDir, "screenpipe-sync.timer");
|
|
10037
|
+
const service = `[Unit]
|
|
10038
|
+
Description=Screenpipe Sync
|
|
10039
|
+
|
|
10040
|
+
[Service]
|
|
10041
|
+
Type=oneshot
|
|
10042
|
+
ExecStart=/bin/bash -c 'export PATH="$HOME/.bun/bin:$PATH" && bunx screenpipe-sync --db ${remotePart} ${outputPart} ${gitPart}'
|
|
10043
|
+
Environment=HOME=${home}
|
|
10044
|
+
|
|
10045
|
+
[Install]
|
|
10046
|
+
WantedBy=default.target`;
|
|
10047
|
+
const timer = `[Unit]
|
|
10048
|
+
Description=Screenpipe Sync Timer
|
|
10049
|
+
|
|
10050
|
+
[Timer]
|
|
10051
|
+
OnBootSec=60
|
|
10052
|
+
OnUnitActiveSec=${config.daemonInterval}s
|
|
10053
|
+
Persistent=true
|
|
10054
|
+
|
|
10055
|
+
[Install]
|
|
10056
|
+
WantedBy=timers.target`;
|
|
10057
|
+
await fs2.mkdir(serviceDir, { recursive: true });
|
|
10058
|
+
await fs2.writeFile(servicePath, service);
|
|
10059
|
+
await fs2.writeFile(timerPath, timer);
|
|
10060
|
+
try {
|
|
10061
|
+
execSync("systemctl --user daemon-reload");
|
|
10062
|
+
execSync("systemctl --user enable --now screenpipe-sync.timer");
|
|
10063
|
+
} catch (e2) {
|
|
10064
|
+
console.error(`[error] Failed to enable systemd timer: ${e2}`);
|
|
10065
|
+
process.exit(1);
|
|
10066
|
+
}
|
|
10067
|
+
console.log(`[ok] Daemon installed (systemd user timer)`);
|
|
10068
|
+
console.log(` Syncs every ${config.daemonInterval}s to: ${config.remote || config.outputDir}`);
|
|
10069
|
+
console.log(` Status: systemctl --user status screenpipe-sync.timer`);
|
|
10070
|
+
console.log(` Stop: bunx screenpipe-sync --stop`);
|
|
10071
|
+
} else {
|
|
10072
|
+
console.error(`[error] Daemon not supported on ${platform}`);
|
|
10073
|
+
console.error(` Use cron instead: */60 * * * * bunx screenpipe-sync --db ${remotePart}`);
|
|
10074
|
+
process.exit(1);
|
|
10075
|
+
}
|
|
10076
|
+
}
|
|
10077
|
+
async function stopDaemon() {
|
|
10078
|
+
const { execSync } = await import("child_process");
|
|
10079
|
+
const os = await import("os");
|
|
10080
|
+
const fs2 = await import("fs/promises");
|
|
10081
|
+
const path = await import("path");
|
|
10082
|
+
const home = os.homedir();
|
|
10083
|
+
const platform = os.platform();
|
|
10084
|
+
if (platform === "darwin") {
|
|
10085
|
+
const plistPath = path.join(home, "Library/LaunchAgents/com.screenpipe.sync.plist");
|
|
10086
|
+
try {
|
|
10087
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`);
|
|
10088
|
+
await fs2.unlink(plistPath);
|
|
10089
|
+
console.log(`[ok] Daemon stopped and removed`);
|
|
10090
|
+
} catch {
|
|
10091
|
+
console.log(`[ok] Daemon was not running`);
|
|
10092
|
+
}
|
|
10093
|
+
} else if (platform === "linux") {
|
|
10094
|
+
try {
|
|
10095
|
+
execSync("systemctl --user disable --now screenpipe-sync.timer 2>/dev/null");
|
|
10096
|
+
console.log(`[ok] Daemon stopped and disabled`);
|
|
10097
|
+
} catch {
|
|
10098
|
+
console.log(`[ok] Daemon was not running`);
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
10102
|
+
async function syncDatabase(config) {
|
|
10103
|
+
const fs2 = await import("fs/promises");
|
|
10104
|
+
const { execSync } = await import("child_process");
|
|
10105
|
+
try {
|
|
10106
|
+
await fs2.access(config.dbPath);
|
|
10107
|
+
} catch {
|
|
10108
|
+
console.error(`[error] Database not found at ${config.dbPath}`);
|
|
10109
|
+
console.error(` Set --db-path or SCREENPIPE_DB environment variable`);
|
|
10110
|
+
process.exit(1);
|
|
10111
|
+
}
|
|
10112
|
+
const stats = await fs2.stat(config.dbPath);
|
|
10113
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
10114
|
+
console.error(`[db] Found database: ${config.dbPath} (${sizeMB} MB)`);
|
|
10115
|
+
if (!config.remote && !config.outputDir) {
|
|
10116
|
+
console.error(`[error] --db requires --remote or --output to specify destination`);
|
|
10117
|
+
process.exit(1);
|
|
10118
|
+
}
|
|
10119
|
+
if (config.outputDir) {
|
|
10120
|
+
const path = await import("path");
|
|
10121
|
+
const destDir = path.resolve(config.outputDir);
|
|
10122
|
+
await fs2.mkdir(destDir, { recursive: true });
|
|
10123
|
+
const destPath = path.join(destDir, "db.sqlite");
|
|
10124
|
+
console.error(`[db] Copying to ${destPath}...`);
|
|
10125
|
+
await fs2.copyFile(config.dbPath, destPath);
|
|
10126
|
+
try {
|
|
10127
|
+
await fs2.copyFile(`${config.dbPath}-wal`, `${destPath}-wal`);
|
|
10128
|
+
await fs2.copyFile(`${config.dbPath}-shm`, `${destPath}-shm`);
|
|
10129
|
+
} catch {}
|
|
10130
|
+
console.error(`[ok] Database copied to ${destPath}`);
|
|
10131
|
+
if (config.gitPush) {
|
|
10132
|
+
try {
|
|
10133
|
+
execSync(`cd "${destDir}" && git add -A && git commit -m "db sync $(date +%Y-%m-%d)" && git push`, {
|
|
10134
|
+
stdio: config.verbose ? "inherit" : "pipe"
|
|
10135
|
+
});
|
|
10136
|
+
console.error(`[ok] Git pushed`);
|
|
10137
|
+
} catch {
|
|
10138
|
+
console.error(`[warn] Git push failed - maybe no changes?`);
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
10141
|
+
}
|
|
10142
|
+
if (config.remote) {
|
|
10143
|
+
console.error(`[db] Syncing to ${config.remote}...`);
|
|
10144
|
+
try {
|
|
10145
|
+
execSync(`rsync -avz --progress "${config.dbPath}" "${config.remote}/db.sqlite"`, {
|
|
10146
|
+
stdio: config.verbose ? "inherit" : "pipe"
|
|
10147
|
+
});
|
|
10148
|
+
console.error(`[ok] Database synced to ${config.remote}`);
|
|
10149
|
+
} catch {
|
|
10150
|
+
try {
|
|
10151
|
+
execSync(`scp "${config.dbPath}" "${config.remote}/db.sqlite"`, {
|
|
10152
|
+
stdio: config.verbose ? "inherit" : "pipe"
|
|
10153
|
+
});
|
|
10154
|
+
console.error(`[ok] Database copied to ${config.remote}`);
|
|
10155
|
+
} catch (e2) {
|
|
10156
|
+
console.error(`[error] Failed to sync database: ${e2}`);
|
|
10157
|
+
process.exit(1);
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
10161
|
+
console.error(`[done] Database sync complete`);
|
|
10162
|
+
}
|
|
9924
10163
|
async function main() {
|
|
9925
10164
|
const config = parseArgs();
|
|
9926
10165
|
const today = new Date().toISOString().split("T")[0];
|
|
10166
|
+
if (config.daemonStop) {
|
|
10167
|
+
await stopDaemon();
|
|
10168
|
+
return;
|
|
10169
|
+
}
|
|
10170
|
+
if (config.daemon) {
|
|
10171
|
+
await setupDaemon(config);
|
|
10172
|
+
return;
|
|
10173
|
+
}
|
|
10174
|
+
if (config.dbSync) {
|
|
10175
|
+
await syncDatabase(config);
|
|
10176
|
+
return;
|
|
10177
|
+
}
|
|
9927
10178
|
console.error(`[screenpipe-sync] Analyzing last ${config.hours} hours...`);
|
|
9928
10179
|
const results = await queryScreenpipe(config);
|
|
9929
10180
|
console.error(`[ok] Retrieved ${results.length} screen captures`);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -51,6 +51,11 @@ interface Config {
|
|
|
51
51
|
ollamaModel: string;
|
|
52
52
|
format: "markdown" | "json";
|
|
53
53
|
verbose: boolean;
|
|
54
|
+
dbSync: boolean;
|
|
55
|
+
dbPath: string;
|
|
56
|
+
daemon: boolean;
|
|
57
|
+
daemonInterval: number;
|
|
58
|
+
daemonStop: boolean;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// ============================================================================
|
|
@@ -59,6 +64,7 @@ interface Config {
|
|
|
59
64
|
|
|
60
65
|
function parseArgs(): Config {
|
|
61
66
|
const args = process.argv.slice(2);
|
|
67
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
62
68
|
const config: Config = {
|
|
63
69
|
screenpipeUrl: process.env.SCREENPIPE_URL || "http://localhost:3030",
|
|
64
70
|
outputDir: null,
|
|
@@ -71,6 +77,11 @@ function parseArgs(): Config {
|
|
|
71
77
|
ollamaModel: process.env.OLLAMA_MODEL || "llama3.2",
|
|
72
78
|
format: "markdown",
|
|
73
79
|
verbose: false,
|
|
80
|
+
dbSync: false,
|
|
81
|
+
dbPath: process.env.SCREENPIPE_DB || `${home}/.screenpipe/db.sqlite`,
|
|
82
|
+
daemon: false,
|
|
83
|
+
daemonInterval: 3600,
|
|
84
|
+
daemonStop: false,
|
|
74
85
|
};
|
|
75
86
|
|
|
76
87
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -99,6 +110,24 @@ function parseArgs(): Config {
|
|
|
99
110
|
case "-v":
|
|
100
111
|
config.verbose = true;
|
|
101
112
|
break;
|
|
113
|
+
case "--db":
|
|
114
|
+
case "--db-sync":
|
|
115
|
+
config.dbSync = true;
|
|
116
|
+
break;
|
|
117
|
+
case "--db-path":
|
|
118
|
+
config.dbPath = args[++i];
|
|
119
|
+
break;
|
|
120
|
+
case "--daemon":
|
|
121
|
+
case "-d":
|
|
122
|
+
config.daemon = true;
|
|
123
|
+
config.dbSync = true; // daemon mode always syncs DB
|
|
124
|
+
break;
|
|
125
|
+
case "--interval":
|
|
126
|
+
config.daemonInterval = parseInt(args[++i]) || 3600;
|
|
127
|
+
break;
|
|
128
|
+
case "--stop":
|
|
129
|
+
config.daemonStop = true;
|
|
130
|
+
break;
|
|
102
131
|
case "--help":
|
|
103
132
|
printHelp();
|
|
104
133
|
process.exit(0);
|
|
@@ -110,10 +139,14 @@ function parseArgs(): Config {
|
|
|
110
139
|
|
|
111
140
|
function printHelp() {
|
|
112
141
|
console.log(`
|
|
113
|
-
|
|
142
|
+
screenpipe-sync - Extract daily context from Screenpipe
|
|
114
143
|
|
|
115
144
|
USAGE:
|
|
116
|
-
bunx
|
|
145
|
+
bunx screenpipe-sync [options]
|
|
146
|
+
|
|
147
|
+
MODES:
|
|
148
|
+
Summary mode (default): AI-powered daily summary extraction
|
|
149
|
+
DB sync mode (--db): Copy raw SQLite database to remote
|
|
117
150
|
|
|
118
151
|
OPTIONS:
|
|
119
152
|
-o, --output <dir> Save summary to directory (default: stdout)
|
|
@@ -123,18 +156,38 @@ OPTIONS:
|
|
|
123
156
|
--json Output as JSON instead of markdown
|
|
124
157
|
-v, --verbose Show debug output
|
|
125
158
|
|
|
159
|
+
--db, --db-sync Sync raw SQLite database instead of summary
|
|
160
|
+
--db-path <path> Path to Screenpipe DB (default: ~/.screenpipe/db.sqlite)
|
|
161
|
+
|
|
162
|
+
-d, --daemon Install persistent background sync (survives reboot)
|
|
163
|
+
--interval <secs> Sync interval in seconds (default: 3600 = 1 hour)
|
|
164
|
+
--stop Stop and remove the daemon
|
|
165
|
+
|
|
126
166
|
ENVIRONMENT:
|
|
127
167
|
SCREENPIPE_URL Screenpipe API URL (default: http://localhost:3030)
|
|
128
|
-
|
|
168
|
+
SCREENPIPE_DB Path to Screenpipe database
|
|
169
|
+
ANTHROPIC_API_KEY For AI summarization (or OPENAI_API_KEY)
|
|
129
170
|
|
|
130
171
|
EXAMPLES:
|
|
131
|
-
|
|
132
|
-
bunx
|
|
133
|
-
|
|
134
|
-
|
|
172
|
+
# AI summary to stdout
|
|
173
|
+
bunx screenpipe-sync
|
|
174
|
+
|
|
175
|
+
# Save daily summaries locally
|
|
176
|
+
bunx screenpipe-sync --output ~/Documents/brain/context --git
|
|
177
|
+
|
|
178
|
+
# Sync raw database to remote (e.g., Clawdbot)
|
|
179
|
+
bunx screenpipe-sync --db --remote user@clawdbot:~/.screenpipe/
|
|
180
|
+
|
|
181
|
+
# Full sync: DB + daily summary
|
|
182
|
+
bunx screenpipe-sync --db -r clawdbot:~/.screenpipe && bunx screenpipe-sync -o ~/context -g
|
|
183
|
+
|
|
184
|
+
# ONE-LINER: Permanent background sync (survives reboot)
|
|
185
|
+
bunx screenpipe-sync --daemon --remote user@server:~/.screenpipe/
|
|
135
186
|
|
|
136
|
-
|
|
137
|
-
|
|
187
|
+
# Stop the daemon
|
|
188
|
+
bunx screenpipe-sync --stop
|
|
189
|
+
|
|
190
|
+
OUTPUT (summary mode):
|
|
138
191
|
- Todo items extracted from screen content
|
|
139
192
|
- Goals and intentions mentioned
|
|
140
193
|
- Decisions made
|
|
@@ -142,6 +195,10 @@ OUTPUT:
|
|
|
142
195
|
- Meetings and conversations
|
|
143
196
|
- Blockers and problems
|
|
144
197
|
- AI-generated insights
|
|
198
|
+
|
|
199
|
+
OUTPUT (db mode):
|
|
200
|
+
- Copies ~/.screenpipe/db.sqlite to remote
|
|
201
|
+
- Remote can query SQLite directly for full history
|
|
145
202
|
`);
|
|
146
203
|
}
|
|
147
204
|
|
|
@@ -435,10 +492,254 @@ async function writeOutput(content: string, config: Config, filename: string) {
|
|
|
435
492
|
// Main
|
|
436
493
|
// ============================================================================
|
|
437
494
|
|
|
495
|
+
async function setupDaemon(config: Config) {
|
|
496
|
+
const fs = await import("fs/promises");
|
|
497
|
+
const { execSync } = await import("child_process");
|
|
498
|
+
const os = await import("os");
|
|
499
|
+
const path = await import("path");
|
|
500
|
+
|
|
501
|
+
const home = os.homedir();
|
|
502
|
+
const platform = os.platform();
|
|
503
|
+
|
|
504
|
+
if (!config.remote && !config.outputDir) {
|
|
505
|
+
console.error(`[error] --daemon requires --remote or --output`);
|
|
506
|
+
console.error(` Example: bunx screenpipe-sync --daemon -r user@host:~/.screenpipe/`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const remotePart = config.remote ? `--remote ${config.remote}` : "";
|
|
511
|
+
const outputPart = config.outputDir ? `--output ${config.outputDir}` : "";
|
|
512
|
+
const gitPart = config.gitPush ? "--git" : "";
|
|
513
|
+
|
|
514
|
+
if (platform === "darwin") {
|
|
515
|
+
// macOS: LaunchAgent
|
|
516
|
+
const plistPath = path.join(home, "Library/LaunchAgents/com.screenpipe.sync.plist");
|
|
517
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
518
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
519
|
+
<plist version="1.0">
|
|
520
|
+
<dict>
|
|
521
|
+
<key>Label</key>
|
|
522
|
+
<string>com.screenpipe.sync</string>
|
|
523
|
+
<key>ProgramArguments</key>
|
|
524
|
+
<array>
|
|
525
|
+
<string>/bin/bash</string>
|
|
526
|
+
<string>-c</string>
|
|
527
|
+
<string>export PATH="$HOME/.bun/bin:/usr/local/bin:/opt/homebrew/bin:$PATH" && bunx screenpipe-sync --db ${remotePart} ${outputPart} ${gitPart}</string>
|
|
528
|
+
</array>
|
|
529
|
+
<key>StartInterval</key>
|
|
530
|
+
<integer>${config.daemonInterval}</integer>
|
|
531
|
+
<key>RunAtLoad</key>
|
|
532
|
+
<true/>
|
|
533
|
+
<key>StandardOutPath</key>
|
|
534
|
+
<string>/tmp/screenpipe-sync.log</string>
|
|
535
|
+
<key>StandardErrorPath</key>
|
|
536
|
+
<string>/tmp/screenpipe-sync.err</string>
|
|
537
|
+
<key>EnvironmentVariables</key>
|
|
538
|
+
<dict>
|
|
539
|
+
<key>HOME</key>
|
|
540
|
+
<string>${home}</string>
|
|
541
|
+
</dict>
|
|
542
|
+
</dict>
|
|
543
|
+
</plist>`;
|
|
544
|
+
|
|
545
|
+
await fs.mkdir(path.dirname(plistPath), { recursive: true });
|
|
546
|
+
await fs.writeFile(plistPath, plist);
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`);
|
|
550
|
+
execSync(`launchctl load "${plistPath}"`);
|
|
551
|
+
} catch (e) {
|
|
552
|
+
console.error(`[error] Failed to load LaunchAgent: ${e}`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
console.log(`[ok] Daemon installed (macOS LaunchAgent)`);
|
|
557
|
+
console.log(` Syncs every ${config.daemonInterval}s to: ${config.remote || config.outputDir}`);
|
|
558
|
+
console.log(` Logs: /tmp/screenpipe-sync.log`);
|
|
559
|
+
console.log(` Stop: bunx screenpipe-sync --stop`);
|
|
560
|
+
|
|
561
|
+
} else if (platform === "linux") {
|
|
562
|
+
// Linux: systemd user service
|
|
563
|
+
const serviceDir = path.join(home, ".config/systemd/user");
|
|
564
|
+
const servicePath = path.join(serviceDir, "screenpipe-sync.service");
|
|
565
|
+
const timerPath = path.join(serviceDir, "screenpipe-sync.timer");
|
|
566
|
+
|
|
567
|
+
const service = `[Unit]
|
|
568
|
+
Description=Screenpipe Sync
|
|
569
|
+
|
|
570
|
+
[Service]
|
|
571
|
+
Type=oneshot
|
|
572
|
+
ExecStart=/bin/bash -c 'export PATH="$HOME/.bun/bin:$PATH" && bunx screenpipe-sync --db ${remotePart} ${outputPart} ${gitPart}'
|
|
573
|
+
Environment=HOME=${home}
|
|
574
|
+
|
|
575
|
+
[Install]
|
|
576
|
+
WantedBy=default.target`;
|
|
577
|
+
|
|
578
|
+
const timer = `[Unit]
|
|
579
|
+
Description=Screenpipe Sync Timer
|
|
580
|
+
|
|
581
|
+
[Timer]
|
|
582
|
+
OnBootSec=60
|
|
583
|
+
OnUnitActiveSec=${config.daemonInterval}s
|
|
584
|
+
Persistent=true
|
|
585
|
+
|
|
586
|
+
[Install]
|
|
587
|
+
WantedBy=timers.target`;
|
|
588
|
+
|
|
589
|
+
await fs.mkdir(serviceDir, { recursive: true });
|
|
590
|
+
await fs.writeFile(servicePath, service);
|
|
591
|
+
await fs.writeFile(timerPath, timer);
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
execSync("systemctl --user daemon-reload");
|
|
595
|
+
execSync("systemctl --user enable --now screenpipe-sync.timer");
|
|
596
|
+
} catch (e) {
|
|
597
|
+
console.error(`[error] Failed to enable systemd timer: ${e}`);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
console.log(`[ok] Daemon installed (systemd user timer)`);
|
|
602
|
+
console.log(` Syncs every ${config.daemonInterval}s to: ${config.remote || config.outputDir}`);
|
|
603
|
+
console.log(` Status: systemctl --user status screenpipe-sync.timer`);
|
|
604
|
+
console.log(` Stop: bunx screenpipe-sync --stop`);
|
|
605
|
+
|
|
606
|
+
} else {
|
|
607
|
+
console.error(`[error] Daemon not supported on ${platform}`);
|
|
608
|
+
console.error(` Use cron instead: */60 * * * * bunx screenpipe-sync --db ${remotePart}`);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function stopDaemon() {
|
|
614
|
+
const { execSync } = await import("child_process");
|
|
615
|
+
const os = await import("os");
|
|
616
|
+
const fs = await import("fs/promises");
|
|
617
|
+
const path = await import("path");
|
|
618
|
+
|
|
619
|
+
const home = os.homedir();
|
|
620
|
+
const platform = os.platform();
|
|
621
|
+
|
|
622
|
+
if (platform === "darwin") {
|
|
623
|
+
const plistPath = path.join(home, "Library/LaunchAgents/com.screenpipe.sync.plist");
|
|
624
|
+
try {
|
|
625
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`);
|
|
626
|
+
await fs.unlink(plistPath);
|
|
627
|
+
console.log(`[ok] Daemon stopped and removed`);
|
|
628
|
+
} catch {
|
|
629
|
+
console.log(`[ok] Daemon was not running`);
|
|
630
|
+
}
|
|
631
|
+
} else if (platform === "linux") {
|
|
632
|
+
try {
|
|
633
|
+
execSync("systemctl --user disable --now screenpipe-sync.timer 2>/dev/null");
|
|
634
|
+
console.log(`[ok] Daemon stopped and disabled`);
|
|
635
|
+
} catch {
|
|
636
|
+
console.log(`[ok] Daemon was not running`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function syncDatabase(config: Config) {
|
|
642
|
+
const fs = await import("fs/promises");
|
|
643
|
+
const { execSync } = await import("child_process");
|
|
644
|
+
|
|
645
|
+
// Check if DB exists
|
|
646
|
+
try {
|
|
647
|
+
await fs.access(config.dbPath);
|
|
648
|
+
} catch {
|
|
649
|
+
console.error(`[error] Database not found at ${config.dbPath}`);
|
|
650
|
+
console.error(` Set --db-path or SCREENPIPE_DB environment variable`);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const stats = await fs.stat(config.dbPath);
|
|
655
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
656
|
+
console.error(`[db] Found database: ${config.dbPath} (${sizeMB} MB)`);
|
|
657
|
+
|
|
658
|
+
if (!config.remote && !config.outputDir) {
|
|
659
|
+
console.error(`[error] --db requires --remote or --output to specify destination`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Copy to local output dir
|
|
664
|
+
if (config.outputDir) {
|
|
665
|
+
const path = await import("path");
|
|
666
|
+
const destDir = path.resolve(config.outputDir);
|
|
667
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
668
|
+
const destPath = path.join(destDir, "db.sqlite");
|
|
669
|
+
|
|
670
|
+
console.error(`[db] Copying to ${destPath}...`);
|
|
671
|
+
await fs.copyFile(config.dbPath, destPath);
|
|
672
|
+
|
|
673
|
+
// Also copy WAL files if they exist (for consistency)
|
|
674
|
+
try {
|
|
675
|
+
await fs.copyFile(`${config.dbPath}-wal`, `${destPath}-wal`);
|
|
676
|
+
await fs.copyFile(`${config.dbPath}-shm`, `${destPath}-shm`);
|
|
677
|
+
} catch {
|
|
678
|
+
// WAL files may not exist, that's ok
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
console.error(`[ok] Database copied to ${destPath}`);
|
|
682
|
+
|
|
683
|
+
if (config.gitPush) {
|
|
684
|
+
try {
|
|
685
|
+
execSync(`cd "${destDir}" && git add -A && git commit -m "db sync $(date +%Y-%m-%d)" && git push`, {
|
|
686
|
+
stdio: config.verbose ? "inherit" : "pipe",
|
|
687
|
+
});
|
|
688
|
+
console.error(`[ok] Git pushed`);
|
|
689
|
+
} catch {
|
|
690
|
+
console.error(`[warn] Git push failed - maybe no changes?`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Sync to remote
|
|
696
|
+
if (config.remote) {
|
|
697
|
+
console.error(`[db] Syncing to ${config.remote}...`);
|
|
698
|
+
try {
|
|
699
|
+
// Use rsync for efficiency (only transfers changes)
|
|
700
|
+
execSync(`rsync -avz --progress "${config.dbPath}" "${config.remote}/db.sqlite"`, {
|
|
701
|
+
stdio: config.verbose ? "inherit" : "pipe",
|
|
702
|
+
});
|
|
703
|
+
console.error(`[ok] Database synced to ${config.remote}`);
|
|
704
|
+
} catch {
|
|
705
|
+
// Fallback to scp if rsync not available
|
|
706
|
+
try {
|
|
707
|
+
execSync(`scp "${config.dbPath}" "${config.remote}/db.sqlite"`, {
|
|
708
|
+
stdio: config.verbose ? "inherit" : "pipe",
|
|
709
|
+
});
|
|
710
|
+
console.error(`[ok] Database copied to ${config.remote}`);
|
|
711
|
+
} catch (e) {
|
|
712
|
+
console.error(`[error] Failed to sync database: ${e}`);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
console.error(`[done] Database sync complete`);
|
|
719
|
+
}
|
|
720
|
+
|
|
438
721
|
async function main() {
|
|
439
722
|
const config = parseArgs();
|
|
440
723
|
const today = new Date().toISOString().split("T")[0];
|
|
441
724
|
|
|
725
|
+
// Stop daemon
|
|
726
|
+
if (config.daemonStop) {
|
|
727
|
+
await stopDaemon();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Daemon mode - install persistent sync
|
|
732
|
+
if (config.daemon) {
|
|
733
|
+
await setupDaemon(config);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// DB sync mode
|
|
738
|
+
if (config.dbSync) {
|
|
739
|
+
await syncDatabase(config);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
442
743
|
console.error(`[screenpipe-sync] Analyzing last ${config.hours} hours...`);
|
|
443
744
|
|
|
444
745
|
// 1. Query Screenpipe
|