botsync 0.1.0 → 0.2.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.
@@ -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
  }
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,14 @@
1
+ /**
2
+ * heartbeat.ts — Periodic heartbeat to the botsync relay.
3
+ *
4
+ * Sends device info every 60s so the dashboard at botsync.io/dashboard
5
+ * can show connected devices. The relay stores each heartbeat in KV
6
+ * with a 5-minute TTL — if we stop heartbeating, we disappear.
7
+ *
8
+ * The heartbeat runs in the background and never throws — a failed
9
+ * heartbeat just means we're invisible on the dashboard for a cycle.
10
+ */
11
+ /** Start the heartbeat loop. Sends one immediately, then every 60s. */
12
+ export declare function startHeartbeat(): void;
13
+ /** Stop the heartbeat loop. */
14
+ export declare function stopHeartbeat(): void;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * heartbeat.ts — Periodic heartbeat to the botsync relay.
4
+ *
5
+ * Sends device info every 60s so the dashboard at botsync.io/dashboard
6
+ * can show connected devices. The relay stores each heartbeat in KV
7
+ * with a 5-minute TTL — if we stop heartbeating, we disappear.
8
+ *
9
+ * The heartbeat runs in the background and never throws — a failed
10
+ * heartbeat just means we're invisible on the dashboard for a cycle.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.startHeartbeat = startHeartbeat;
14
+ exports.stopHeartbeat = stopHeartbeat;
15
+ const os_1 = require("os");
16
+ const config_js_1 = require("./config.js");
17
+ const RELAY_URL = "https://relay.botsync.io";
18
+ const HEARTBEAT_INTERVAL_MS = 60_000;
19
+ let heartbeatTimer = null;
20
+ /** Send a single heartbeat to the relay. */
21
+ async function sendHeartbeat() {
22
+ const config = (0, config_js_1.readConfig)();
23
+ const networkId = (0, config_js_1.readNetworkId)();
24
+ if (!config?.deviceId || !networkId)
25
+ return;
26
+ try {
27
+ await fetch(`${RELAY_URL}/network/${encodeURIComponent(networkId)}/heartbeat`, {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify({
31
+ deviceId: config.deviceId,
32
+ name: (0, os_1.hostname)(),
33
+ os: process.platform,
34
+ version: getVersion(),
35
+ }),
36
+ signal: AbortSignal.timeout(5000),
37
+ });
38
+ }
39
+ catch {
40
+ // Silent fail — dashboard visibility is best-effort
41
+ }
42
+ }
43
+ /** Start the heartbeat loop. Sends one immediately, then every 60s. */
44
+ function startHeartbeat() {
45
+ if (heartbeatTimer)
46
+ return; // Already running
47
+ // Fire immediately so the device shows up right away
48
+ sendHeartbeat();
49
+ heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
50
+ // Don't let the heartbeat timer keep the process alive if
51
+ // everything else is done (shouldn't happen since Syncthing
52
+ // daemon is the main keep-alive, but just in case)
53
+ if (heartbeatTimer.unref) {
54
+ heartbeatTimer.unref();
55
+ }
56
+ }
57
+ /** Stop the heartbeat loop. */
58
+ function stopHeartbeat() {
59
+ if (heartbeatTimer) {
60
+ clearInterval(heartbeatTimer);
61
+ heartbeatTimer = null;
62
+ }
63
+ }
64
+ /** Read package version, fallback to 0.0.0 */
65
+ function getVersion() {
66
+ try {
67
+ const pkg = require("../package.json");
68
+ return pkg.version || "0.0.0";
69
+ }
70
+ catch {
71
+ return "0.0.0";
72
+ }
73
+ }
@@ -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,9 +1,9 @@
1
1
  {
2
2
  "name": "botsync",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "P2P file sync for AI agents. Syncthing under the hood. Two commands.",
5
5
  "bin": {
6
- "botsync": "./dist/cli.js"
6
+ "botsync": "dist/cli.js"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "tsc",
@@ -20,7 +20,7 @@
20
20
  "homepage": "https://botsync.io",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/hashbranch/botsync"
23
+ "url": "git+https://github.com/hashbranch/botsync.git"
24
24
  },
25
25
  "license": "MIT",
26
26
  "files": [