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 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 - outputs to stdout
11
- bunx @screenpipe/sync
10
+ # One-liner - AI summary to stdout
11
+ bunx screenpipe-sync
12
12
 
13
- # Save to a folder (creates YYYY-MM-DD.md files)
14
- bunx @screenpipe/sync --output ~/Documents/brain/context
13
+ # Save daily summaries locally
14
+ bunx screenpipe-sync --output ~/Documents/brain/context --git
15
15
 
16
- # Auto commit and push
17
- bunx @screenpipe/sync --output ~/notes --git
16
+ # Sync raw SQLite database to remote (full history!)
17
+ bunx screenpipe-sync --db --remote user@host:~/.screenpipe/
18
18
 
19
- # Sync to remote server (e.g., Clawdbot)
20
- bunx @screenpipe/sync --remote user@host:~/brain/context
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
- @screenpipe/sync - Extract daily context from Screenpipe
9678
+ screenpipe-sync - Extract daily context from Screenpipe
9655
9679
 
9656
9680
  USAGE:
9657
- bunx @screenpipe/sync [options]
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
- ANTHROPIC_API_KEY Required for AI summarization
9704
+ SCREENPIPE_DB Path to Screenpipe database
9705
+ ANTHROPIC_API_KEY For AI summarization (or OPENAI_API_KEY)
9670
9706
 
9671
9707
  EXAMPLES:
9672
- bunx @screenpipe/sync
9673
- bunx @screenpipe/sync --output ~/Documents/brain/context --git
9674
- bunx @screenpipe/sync --hours 24 --json
9675
- bunx @screenpipe/sync --remote clawdbot:~/brain/context
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
- OUTPUT:
9678
- Creates structured daily summaries with:
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" &amp;&amp; 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-sync",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Sync Screenpipe activity to structured daily summaries. Extract todos, goals, decisions from your screen history.",
5
5
  "type": "module",
6
6
  "bin": {
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
- @screenpipe/sync - Extract daily context from Screenpipe
142
+ screenpipe-sync - Extract daily context from Screenpipe
114
143
 
115
144
  USAGE:
116
- bunx @screenpipe/sync [options]
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
- ANTHROPIC_API_KEY Required for AI summarization
168
+ SCREENPIPE_DB Path to Screenpipe database
169
+ ANTHROPIC_API_KEY For AI summarization (or OPENAI_API_KEY)
129
170
 
130
171
  EXAMPLES:
131
- bunx @screenpipe/sync
132
- bunx @screenpipe/sync --output ~/Documents/brain/context --git
133
- bunx @screenpipe/sync --hours 24 --json
134
- bunx @screenpipe/sync --remote clawdbot:~/brain/context
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
- OUTPUT:
137
- Creates structured daily summaries with:
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" &amp;&amp; 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