botsync 0.1.1 → 0.2.1

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.
@@ -51,6 +51,7 @@ const crypto_1 = require("crypto");
51
51
  const config_js_1 = require("../config.js");
52
52
  const syncthing_js_1 = require("../syncthing.js");
53
53
  const passphrase_js_1 = require("../passphrase.js");
54
+ const heartbeat_js_1 = require("../heartbeat.js");
54
55
  const ui = __importStar(require("../ui.js"));
55
56
  /**
56
57
  * Pick a random port in the ephemeral range for the Syncthing REST API.
@@ -87,11 +88,17 @@ async function init() {
87
88
  spin.succeed();
88
89
  const deviceId = await (0, syncthing_js_1.getDeviceId)();
89
90
  (0, config_js_1.writeConfig)({ apiKey, apiPort, deviceId });
91
+ // Step 5b: Generate network ID and start heartbeat
92
+ const networkId = (0, crypto_1.randomUUID)();
93
+ (0, config_js_1.writeNetworkId)(networkId);
94
+ (0, heartbeat_js_1.startHeartbeat)();
95
+ ui.stepDone("Network registered");
90
96
  // Step 6: Register with relay and display the code
91
97
  ui.gap();
92
98
  const { code, isRelay } = await (0, passphrase_js_1.createCode)({
93
99
  deviceId,
94
100
  folders: config_js_1.FOLDERS.map((f) => f.id),
101
+ networkId,
95
102
  });
96
103
  if (isRelay) {
97
104
  ui.passphraseBox(code, `npx botsync join ${code}`);
@@ -103,6 +110,8 @@ async function init() {
103
110
  ui.passphraseBox(code, `npx botsync join ${code.substring(0, 20)}...`);
104
111
  }
105
112
  ui.gap();
113
+ ui.info(`Dashboard: https://botsync.io/dashboard#${networkId}`);
114
+ ui.gap();
106
115
  // Step 7: Wait for the joining device to connect and auto-accept it
107
116
  const peerSpin = ui.spinner("Waiting for peer...");
108
117
  const accepted = await waitForPeer(apiKey, apiPort, peerSpin);
@@ -51,6 +51,7 @@ const crypto_1 = require("crypto");
51
51
  const config_js_1 = require("../config.js");
52
52
  const syncthing_js_1 = require("../syncthing.js");
53
53
  const passphrase_js_1 = require("../passphrase.js");
54
+ const heartbeat_js_1 = require("../heartbeat.js");
54
55
  const ui = __importStar(require("../ui.js"));
55
56
  async function join(passphrase) {
56
57
  ui.header();
@@ -58,10 +59,12 @@ async function join(passphrase) {
58
59
  const spin0 = ui.spinner("Resolving pairing code...");
59
60
  let remoteId;
60
61
  let folders;
62
+ let networkId;
61
63
  try {
62
64
  const data = await (0, passphrase_js_1.resolveCode)(passphrase);
63
65
  remoteId = data.deviceId;
64
66
  folders = data.folders;
67
+ networkId = data.networkId;
65
68
  spin0.succeed();
66
69
  }
67
70
  catch (err) {
@@ -102,5 +105,14 @@ async function join(passphrase) {
102
105
  await (0, syncthing_js_1.addDeviceToFolder)(folderId, remoteId);
103
106
  }
104
107
  spin2.stop();
105
- ui.connected(remoteId);
108
+ // Save network ID (inherited from init side) and start heartbeat
109
+ if (networkId) {
110
+ (0, config_js_1.writeNetworkId)(networkId);
111
+ (0, heartbeat_js_1.startHeartbeat)();
112
+ ui.connected(remoteId);
113
+ ui.info(`Dashboard: https://botsync.io/dashboard#${networkId}`);
114
+ }
115
+ else {
116
+ ui.connected(remoteId);
117
+ }
106
118
  }
@@ -40,8 +40,10 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.stop = stop;
42
42
  const syncthing_js_1 = require("../syncthing.js");
43
+ const heartbeat_js_1 = require("../heartbeat.js");
43
44
  const ui = __importStar(require("../ui.js"));
44
45
  async function stop() {
46
+ (0, heartbeat_js_1.stopHeartbeat)();
45
47
  const killed = (0, syncthing_js_1.stopDaemon)();
46
48
  if (killed) {
47
49
  ui.stopped();
package/dist/config.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const FOLDERS: {
19
19
  id: string;
20
20
  path: string;
21
21
  }[];
22
+ export declare const NETWORK_FILE: string;
22
23
  /** Runtime config shape — everything we need to talk to Syncthing */
23
24
  export interface BotsyncConfig {
24
25
  apiKey: string;
@@ -29,3 +30,7 @@ export interface BotsyncConfig {
29
30
  export declare function readConfig(): BotsyncConfig | null;
30
31
  /** Write config to disk. Creates parent dirs if needed. */
31
32
  export declare function writeConfig(config: BotsyncConfig): void;
33
+ /** Read the network ID, or return null if not yet assigned */
34
+ export declare function readNetworkId(): string | null;
35
+ /** Write the network ID to disk. */
36
+ export declare function writeNetworkId(networkId: string): void;
package/dist/config.js CHANGED
@@ -10,9 +10,11 @@
10
10
  * and PID so that all commands can talk to the running Syncthing instance.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.FOLDERS = exports.PID_FILE = exports.CONFIG_FILE = exports.SYNCTHING_BIN = exports.SYNCTHING_BIN_DIR = exports.SYNCTHING_CONFIG_DIR = exports.BOTSYNC_DIR = exports.SYNC_DIR = void 0;
13
+ exports.NETWORK_FILE = exports.FOLDERS = exports.PID_FILE = exports.CONFIG_FILE = exports.SYNCTHING_BIN = exports.SYNCTHING_BIN_DIR = exports.SYNCTHING_CONFIG_DIR = exports.BOTSYNC_DIR = exports.SYNC_DIR = void 0;
14
14
  exports.readConfig = readConfig;
15
15
  exports.writeConfig = writeConfig;
16
+ exports.readNetworkId = readNetworkId;
17
+ exports.writeNetworkId = writeNetworkId;
16
18
  const fs_1 = require("fs");
17
19
  const path_1 = require("path");
18
20
  const os_1 = require("os");
@@ -38,6 +40,8 @@ exports.FOLDERS = [
38
40
  { id: "botsync-deliverables", path: (0, path_1.join)(exports.SYNC_DIR, "deliverables") },
39
41
  { id: "botsync-inbox", path: (0, path_1.join)(exports.SYNC_DIR, "inbox") },
40
42
  ];
43
+ // Network identity file — stores the network ID for dashboard visibility
44
+ exports.NETWORK_FILE = (0, path_1.join)(exports.BOTSYNC_DIR, "network.json");
41
45
  /** Read the config file, or return null if it doesn't exist */
42
46
  function readConfig() {
43
47
  try {
@@ -53,3 +57,19 @@ function writeConfig(config) {
53
57
  (0, fs_1.mkdirSync)(exports.BOTSYNC_DIR, { recursive: true });
54
58
  (0, fs_1.writeFileSync)(exports.CONFIG_FILE, JSON.stringify(config, null, 2));
55
59
  }
60
+ /** Read the network ID, or return null if not yet assigned */
61
+ function readNetworkId() {
62
+ try {
63
+ const raw = (0, fs_1.readFileSync)(exports.NETWORK_FILE, "utf-8");
64
+ const data = JSON.parse(raw);
65
+ return data.networkId || null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /** Write the network ID to disk. */
72
+ function writeNetworkId(networkId) {
73
+ (0, fs_1.mkdirSync)(exports.BOTSYNC_DIR, { recursive: true });
74
+ (0, fs_1.writeFileSync)(exports.NETWORK_FILE, JSON.stringify({ networkId }, null, 2));
75
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * heartbeat-daemon.ts — Standalone background heartbeat process.
4
+ *
5
+ * Spawned as a detached child by init/join. Sends heartbeats every 60s
6
+ * until the Syncthing daemon dies (checked via API ping), then exits.
7
+ *
8
+ * Usage: node heartbeat-daemon.js
9
+ * (Not meant to be run directly — spawned by init/join)
10
+ */
11
+ export {};
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * heartbeat-daemon.ts — Standalone background heartbeat process.
5
+ *
6
+ * Spawned as a detached child by init/join. Sends heartbeats every 60s
7
+ * until the Syncthing daemon dies (checked via API ping), then exits.
8
+ *
9
+ * Usage: node heartbeat-daemon.js
10
+ * (Not meant to be run directly — spawned by init/join)
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const os_1 = require("os");
14
+ const config_js_1 = require("./config.js");
15
+ const fs_1 = require("fs");
16
+ const path_1 = require("path");
17
+ const RELAY_URL = "https://relay.botsync.io";
18
+ const HEARTBEAT_INTERVAL_MS = 60_000;
19
+ const HEALTH_CHECK_INTERVAL_MS = 120_000; // Check if Syncthing is alive every 2 min
20
+ const PID_FILE = (0, path_1.join)(config_js_1.BOTSYNC_DIR, "heartbeat.pid");
21
+ function getVersion() {
22
+ try {
23
+ return require("../package.json").version || "0.0.0";
24
+ }
25
+ catch {
26
+ return "0.0.0";
27
+ }
28
+ }
29
+ async function sendHeartbeat() {
30
+ const config = (0, config_js_1.readConfig)();
31
+ const networkId = (0, config_js_1.readNetworkId)();
32
+ if (!config?.deviceId || !networkId)
33
+ return false;
34
+ try {
35
+ const res = await fetch(`${RELAY_URL}/network/${encodeURIComponent(networkId)}/heartbeat`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({
39
+ deviceId: config.deviceId,
40
+ name: (0, os_1.hostname)(),
41
+ os: process.platform,
42
+ version: getVersion(),
43
+ }),
44
+ signal: AbortSignal.timeout(5000),
45
+ });
46
+ return res.ok;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function isSyncthingAlive() {
53
+ const config = (0, config_js_1.readConfig)();
54
+ if (!config)
55
+ return false;
56
+ try {
57
+ const res = await fetch(`http://127.0.0.1:${config.apiPort}/rest/system/ping`, {
58
+ headers: { "X-API-Key": config.apiKey },
59
+ signal: AbortSignal.timeout(3000),
60
+ });
61
+ return res.ok;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ async function main() {
68
+ // Write our PID so stop can kill us
69
+ (0, fs_1.writeFileSync)(PID_FILE, String(process.pid));
70
+ // Cleanup PID file on exit
71
+ const cleanup = () => {
72
+ try {
73
+ (0, fs_1.unlinkSync)(PID_FILE);
74
+ }
75
+ catch { }
76
+ };
77
+ process.on("exit", cleanup);
78
+ process.on("SIGTERM", () => {
79
+ cleanup();
80
+ process.exit(0);
81
+ });
82
+ process.on("SIGINT", () => {
83
+ cleanup();
84
+ process.exit(0);
85
+ });
86
+ // Initial heartbeat
87
+ await sendHeartbeat();
88
+ // Heartbeat loop
89
+ setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
90
+ // Health check loop — exit if Syncthing is gone
91
+ setInterval(async () => {
92
+ const alive = await isSyncthingAlive();
93
+ if (!alive) {
94
+ cleanup();
95
+ process.exit(0);
96
+ }
97
+ }, HEALTH_CHECK_INTERVAL_MS);
98
+ }
99
+ main().catch(() => process.exit(1));
@@ -0,0 +1,13 @@
1
+ /**
2
+ * heartbeat.ts — Spawn/stop the background heartbeat daemon.
3
+ *
4
+ * Instead of running in-process (which dies when init/join exits),
5
+ * we spawn heartbeat-daemon.js as a detached child process that
6
+ * outlives the parent CLI command.
7
+ */
8
+ /** Spawn the heartbeat daemon as a detached background process. */
9
+ export declare function startHeartbeat(): void;
10
+ /** Stop the heartbeat daemon if running. */
11
+ export declare function stopHeartbeat(): void;
12
+ /** Check if the heartbeat daemon is still alive. */
13
+ export declare function isHeartbeatRunning(): boolean;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * heartbeat.ts — Spawn/stop the background heartbeat daemon.
4
+ *
5
+ * Instead of running in-process (which dies when init/join exits),
6
+ * we spawn heartbeat-daemon.js as a detached child process that
7
+ * outlives the parent CLI command.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.startHeartbeat = startHeartbeat;
11
+ exports.stopHeartbeat = stopHeartbeat;
12
+ exports.isHeartbeatRunning = isHeartbeatRunning;
13
+ const child_process_1 = require("child_process");
14
+ const fs_1 = require("fs");
15
+ const path_1 = require("path");
16
+ const config_js_1 = require("./config.js");
17
+ const HEARTBEAT_PID_FILE = (0, path_1.join)(config_js_1.BOTSYNC_DIR, "heartbeat.pid");
18
+ /** Spawn the heartbeat daemon as a detached background process. */
19
+ function startHeartbeat() {
20
+ // Don't double-spawn
21
+ if (isHeartbeatRunning())
22
+ return;
23
+ const daemonScript = (0, path_1.join)((0, path_1.dirname)(__filename), "heartbeat-daemon.js");
24
+ const child = (0, child_process_1.spawn)(process.execPath, [daemonScript], {
25
+ detached: true,
26
+ stdio: "ignore",
27
+ });
28
+ child.unref();
29
+ }
30
+ /** Stop the heartbeat daemon if running. */
31
+ function stopHeartbeat() {
32
+ const pid = getHeartbeatPid();
33
+ if (pid) {
34
+ try {
35
+ process.kill(pid, "SIGTERM");
36
+ }
37
+ catch {
38
+ // Already dead
39
+ }
40
+ }
41
+ }
42
+ /** Check if the heartbeat daemon is still alive. */
43
+ function isHeartbeatRunning() {
44
+ const pid = getHeartbeatPid();
45
+ if (!pid)
46
+ return false;
47
+ try {
48
+ process.kill(pid, 0); // Signal 0 = just check if alive
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ function getHeartbeatPid() {
56
+ try {
57
+ if (!(0, fs_1.existsSync)(HEARTBEAT_PID_FILE))
58
+ return null;
59
+ const raw = (0, fs_1.readFileSync)(HEARTBEAT_PID_FILE, "utf-8").trim();
60
+ const pid = parseInt(raw, 10);
61
+ return isNaN(pid) ? null : pid;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
@@ -12,6 +12,7 @@
12
12
  export interface PassphraseData {
13
13
  deviceId: string;
14
14
  folders: string[];
15
+ networkId?: string;
15
16
  }
16
17
  /**
17
18
  * Register a device ID with the relay and get a short code.
@@ -32,7 +32,7 @@ async function createCode(data) {
32
32
  const res = await fetch(`${RELAY_URL}/pair`, {
33
33
  method: "POST",
34
34
  headers: { "Content-Type": "application/json" },
35
- body: JSON.stringify({ deviceId: data.deviceId }),
35
+ body: JSON.stringify({ deviceId: data.deviceId, networkId: data.networkId }),
36
36
  signal: AbortSignal.timeout(5000),
37
37
  });
38
38
  if (res.ok) {
@@ -64,11 +64,12 @@ async function resolveCode(code) {
64
64
  const body = (await res.json());
65
65
  throw new Error(body.error || `Relay returned ${res.status}`);
66
66
  }
67
- const { deviceId } = (await res.json());
67
+ const { deviceId, networkId } = (await res.json());
68
68
  // Folders are always the standard set — no need to encode them
69
69
  return {
70
70
  deviceId,
71
71
  folders: ["botsync-shared", "botsync-deliverables", "botsync-inbox"],
72
+ networkId: networkId || undefined,
72
73
  };
73
74
  }
74
75
  // Legacy base58 fallback
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botsync",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "P2P file sync for AI agents. Syncthing under the hood. Two commands.",
5
5
  "bin": {
6
6
  "botsync": "dist/cli.js"