screenpipe-sync 0.2.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.
Files changed (3) hide show
  1. package/dist/index.js +159 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +185 -0
package/dist/index.js CHANGED
@@ -9617,7 +9617,10 @@ function parseArgs() {
9617
9617
  format: "markdown",
9618
9618
  verbose: false,
9619
9619
  dbSync: false,
9620
- dbPath: process.env.SCREENPIPE_DB || `${home}/.screenpipe/db.sqlite`
9620
+ dbPath: process.env.SCREENPIPE_DB || `${home}/.screenpipe/db.sqlite`,
9621
+ daemon: false,
9622
+ daemonInterval: 3600,
9623
+ daemonStop: false
9621
9624
  };
9622
9625
  for (let i2 = 0;i2 < args.length; i2++) {
9623
9626
  const arg = args[i2];
@@ -9652,6 +9655,17 @@ function parseArgs() {
9652
9655
  case "--db-path":
9653
9656
  config.dbPath = args[++i2];
9654
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;
9655
9669
  case "--help":
9656
9670
  printHelp();
9657
9671
  process.exit(0);
@@ -9681,6 +9695,10 @@ OPTIONS:
9681
9695
  --db, --db-sync Sync raw SQLite database instead of summary
9682
9696
  --db-path <path> Path to Screenpipe DB (default: ~/.screenpipe/db.sqlite)
9683
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
+
9684
9702
  ENVIRONMENT:
9685
9703
  SCREENPIPE_URL Screenpipe API URL (default: http://localhost:3030)
9686
9704
  SCREENPIPE_DB Path to Screenpipe database
@@ -9699,6 +9717,12 @@ EXAMPLES:
9699
9717
  # Full sync: DB + daily summary
9700
9718
  bunx screenpipe-sync --db -r clawdbot:~/.screenpipe && bunx screenpipe-sync -o ~/context -g
9701
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
+
9702
9726
  OUTPUT (summary mode):
9703
9727
  - Todo items extracted from screen content
9704
9728
  - Goals and intentions mentioned
@@ -9949,6 +9973,132 @@ async function writeOutput(content, config, filename) {
9949
9973
  }
9950
9974
  }
9951
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
+ }
9952
10102
  async function syncDatabase(config) {
9953
10103
  const fs2 = await import("fs/promises");
9954
10104
  const { execSync } = await import("child_process");
@@ -10013,6 +10163,14 @@ async function syncDatabase(config) {
10013
10163
  async function main() {
10014
10164
  const config = parseArgs();
10015
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
+ }
10016
10174
  if (config.dbSync) {
10017
10175
  await syncDatabase(config);
10018
10176
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-sync",
3
- "version": "0.2.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
@@ -53,6 +53,9 @@ interface Config {
53
53
  verbose: boolean;
54
54
  dbSync: boolean;
55
55
  dbPath: string;
56
+ daemon: boolean;
57
+ daemonInterval: number;
58
+ daemonStop: boolean;
56
59
  }
57
60
 
58
61
  // ============================================================================
@@ -76,6 +79,9 @@ function parseArgs(): Config {
76
79
  verbose: false,
77
80
  dbSync: false,
78
81
  dbPath: process.env.SCREENPIPE_DB || `${home}/.screenpipe/db.sqlite`,
82
+ daemon: false,
83
+ daemonInterval: 3600,
84
+ daemonStop: false,
79
85
  };
80
86
 
81
87
  for (let i = 0; i < args.length; i++) {
@@ -111,6 +117,17 @@ function parseArgs(): Config {
111
117
  case "--db-path":
112
118
  config.dbPath = args[++i];
113
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;
114
131
  case "--help":
115
132
  printHelp();
116
133
  process.exit(0);
@@ -142,6 +159,10 @@ OPTIONS:
142
159
  --db, --db-sync Sync raw SQLite database instead of summary
143
160
  --db-path <path> Path to Screenpipe DB (default: ~/.screenpipe/db.sqlite)
144
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
+
145
166
  ENVIRONMENT:
146
167
  SCREENPIPE_URL Screenpipe API URL (default: http://localhost:3030)
147
168
  SCREENPIPE_DB Path to Screenpipe database
@@ -160,6 +181,12 @@ EXAMPLES:
160
181
  # Full sync: DB + daily summary
161
182
  bunx screenpipe-sync --db -r clawdbot:~/.screenpipe && bunx screenpipe-sync -o ~/context -g
162
183
 
184
+ # ONE-LINER: Permanent background sync (survives reboot)
185
+ bunx screenpipe-sync --daemon --remote user@server:~/.screenpipe/
186
+
187
+ # Stop the daemon
188
+ bunx screenpipe-sync --stop
189
+
163
190
  OUTPUT (summary mode):
164
191
  - Todo items extracted from screen content
165
192
  - Goals and intentions mentioned
@@ -465,6 +492,152 @@ async function writeOutput(content: string, config: Config, filename: string) {
465
492
  // Main
466
493
  // ============================================================================
467
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
+
468
641
  async function syncDatabase(config: Config) {
469
642
  const fs = await import("fs/promises");
470
643
  const { execSync } = await import("child_process");
@@ -549,6 +722,18 @@ async function main() {
549
722
  const config = parseArgs();
550
723
  const today = new Date().toISOString().split("T")[0];
551
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
+
552
737
  // DB sync mode
553
738
  if (config.dbSync) {
554
739
  await syncDatabase(config);