agendex-cli 0.5.0 → 0.7.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 (2) hide show
  1. package/dist/cli.js +270 -182
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2127,11 +2127,13 @@ function normalizeStoredConfig(raw) {
2127
2127
  const token = typeof raw.token === "string" && raw.token.trim() ? raw.token : undefined;
2128
2128
  const cloudToken = typeof raw.cloudToken === "string" && raw.cloudToken.trim() ? raw.cloudToken : undefined;
2129
2129
  const convexUrl = typeof raw.convexUrl === "string" && raw.convexUrl.trim() ? raw.convexUrl : undefined;
2130
+ const deviceId = typeof raw.deviceId === "string" && raw.deviceId.trim() ? raw.deviceId : undefined;
2130
2131
  return {
2131
2132
  configVersion: 3,
2132
2133
  token,
2133
2134
  cloudToken,
2134
2135
  convexUrl,
2136
+ deviceId,
2135
2137
  enabledAdapters: normalizeAdapterIds(raw.enabledAdapters)
2136
2138
  };
2137
2139
  }
@@ -2145,6 +2147,7 @@ function saveConfig(config) {
2145
2147
  token: config.token,
2146
2148
  cloudToken: config.cloudToken,
2147
2149
  convexUrl: config.convexUrl,
2150
+ deviceId: config.deviceId,
2148
2151
  enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters)
2149
2152
  };
2150
2153
  writeFileSync(configPath, JSON.stringify(payload, null, 2));
@@ -2170,6 +2173,19 @@ function loadOrCreateToken() {
2170
2173
  `);
2171
2174
  return token;
2172
2175
  }
2176
+ function loadOrCreateDeviceId() {
2177
+ const existing = loadConfig();
2178
+ if (existing?.deviceId)
2179
+ return existing.deviceId;
2180
+ const deviceId = randomBytes(16).toString("hex");
2181
+ saveConfig({
2182
+ ...existing ?? {},
2183
+ configVersion: 3,
2184
+ deviceId,
2185
+ enabledAdapters: existing?.enabledAdapters ?? []
2186
+ });
2187
+ return deviceId;
2188
+ }
2173
2189
  async function loadOrInitConfig(options = {}) {
2174
2190
  const configureAdapters = Boolean(options.configureAdapters);
2175
2191
  const existing = loadConfig();
@@ -2195,11 +2211,13 @@ async function loadOrInitConfig(options = {}) {
2195
2211
  if (enabledAdapters.length === 0)
2196
2212
  enabledAdapters = getDefaultAdapterIds();
2197
2213
  }
2214
+ const deviceId = existing?.deviceId || randomBytes(16).toString("hex");
2198
2215
  const nextConfig = {
2199
2216
  configVersion: 3,
2200
2217
  token: tokenFromEnv ? existing?.token : currentToken,
2201
2218
  cloudToken: existing?.cloudToken,
2202
2219
  convexUrl: existing?.convexUrl,
2220
+ deviceId,
2203
2221
  enabledAdapters
2204
2222
  };
2205
2223
  saveConfig(nextConfig);
@@ -2474,14 +2492,241 @@ function startWatching(onChange) {
2474
2492
  watchDir(dir, (f) => adapter.matches(f), onChange);
2475
2493
  }
2476
2494
  }
2495
+ // src/api.ts
2496
+ import { request as httpRequest } from "node:http";
2497
+ import { request as httpsRequest } from "node:https";
2498
+ import { hostname as osHostname } from "node:os";
2499
+
2500
+ // src/pid.ts
2501
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2502
+ import { homedir as homedir9, hostname } from "node:os";
2503
+ import { dirname, join as join10 } from "node:path";
2504
+ var pidPath = join10(homedir9(), ".agendex", "daemon.pid");
2505
+ function writePid() {
2506
+ mkdirSync2(dirname(pidPath), { recursive: true });
2507
+ const info = {
2508
+ pid: process.pid,
2509
+ startedAtMs: Date.now(),
2510
+ hostname: hostname()
2511
+ };
2512
+ writeFileSync2(pidPath, JSON.stringify(info));
2513
+ }
2514
+ function readPidInfo() {
2515
+ if (!existsSync6(pidPath))
2516
+ return null;
2517
+ const raw = readFileSync3(pidPath, "utf-8").trim();
2518
+ const asNumber = Number(raw);
2519
+ if (Number.isFinite(asNumber) && asNumber > 0 && !raw.startsWith("{")) {
2520
+ return { pid: asNumber };
2521
+ }
2522
+ try {
2523
+ const parsed = JSON.parse(raw);
2524
+ if (Number.isFinite(parsed.pid) && parsed.pid > 0)
2525
+ return parsed;
2526
+ } catch {}
2527
+ return null;
2528
+ }
2529
+ function readPid() {
2530
+ return readPidInfo()?.pid ?? null;
2531
+ }
2532
+ function removePid() {
2533
+ try {
2534
+ unlinkSync(pidPath);
2535
+ } catch {}
2536
+ }
2537
+ function isRunning(pid) {
2538
+ try {
2539
+ process.kill(pid, 0);
2540
+ return true;
2541
+ } catch {
2542
+ return false;
2543
+ }
2544
+ }
2545
+
2546
+ // src/api.ts
2547
+ var cachedDeviceId;
2548
+ function getCloudConfig() {
2549
+ const config = loadConfig();
2550
+ if (!config?.cloudToken)
2551
+ throw new Error("Not logged in. Run `agendex login` first.");
2552
+ if (!config.convexUrl)
2553
+ throw new Error("No Convex URL configured. Run `agendex login` first.");
2554
+ return { token: config.cloudToken, convexUrl: config.convexUrl };
2555
+ }
2556
+ async function syncPlan(plan) {
2557
+ const { token, convexUrl } = getCloudConfig();
2558
+ const url = `${convexUrl}/api/cli/sync`;
2559
+ let activeToken = token;
2560
+ let res = await requestText(url, {
2561
+ method: "POST",
2562
+ headers: {
2563
+ Authorization: `Bearer ${activeToken}`,
2564
+ Connection: "close",
2565
+ "Content-Type": "application/json"
2566
+ },
2567
+ body: JSON.stringify(plan)
2568
+ });
2569
+ if (res.status === 401) {
2570
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
2571
+ if (refreshed) {
2572
+ activeToken = refreshed;
2573
+ res = await requestText(url, {
2574
+ method: "POST",
2575
+ headers: {
2576
+ Authorization: `Bearer ${activeToken}`,
2577
+ Connection: "close",
2578
+ "Content-Type": "application/json"
2579
+ },
2580
+ body: JSON.stringify(plan)
2581
+ });
2582
+ }
2583
+ }
2584
+ if (res.status < 200 || res.status >= 300) {
2585
+ return { ok: false, error: `${res.status}: ${res.body}` };
2586
+ }
2587
+ return { ok: true };
2588
+ }
2589
+ async function refreshStoredToken(currentToken, convexUrl) {
2590
+ const refreshed = await refreshToken(currentToken, convexUrl);
2591
+ if (!refreshed)
2592
+ return null;
2593
+ const config = loadConfig();
2594
+ if (config) {
2595
+ saveConfig({ ...config, cloudToken: refreshed.token });
2596
+ }
2597
+ return refreshed.token;
2598
+ }
2599
+ async function sendHeartbeat() {
2600
+ try {
2601
+ const { token, convexUrl } = getCloudConfig();
2602
+ const pidInfo = readPidInfo();
2603
+ cachedDeviceId ??= loadOrCreateDeviceId();
2604
+ const heartbeatBody = JSON.stringify({
2605
+ deviceId: cachedDeviceId,
2606
+ hostname: pidInfo?.hostname ?? osHostname(),
2607
+ startedAtMs: pidInfo?.startedAtMs,
2608
+ pid: pidInfo?.pid
2609
+ });
2610
+ let activeToken = token;
2611
+ let res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2612
+ method: "POST",
2613
+ headers: {
2614
+ Authorization: `Bearer ${activeToken}`,
2615
+ Connection: "close",
2616
+ "Content-Type": "application/json"
2617
+ },
2618
+ body: heartbeatBody
2619
+ });
2620
+ if (res.status === 401) {
2621
+ const refreshedToken = await refreshStoredToken(activeToken, convexUrl);
2622
+ if (!refreshedToken)
2623
+ return;
2624
+ activeToken = refreshedToken;
2625
+ res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2626
+ method: "POST",
2627
+ headers: {
2628
+ Authorization: `Bearer ${activeToken}`,
2629
+ Connection: "close",
2630
+ "Content-Type": "application/json"
2631
+ },
2632
+ body: heartbeatBody
2633
+ });
2634
+ }
2635
+ if (res.status === 401) {
2636
+ return;
2637
+ }
2638
+ } catch {}
2639
+ }
2640
+ async function refreshToken(currentToken, convexUrl) {
2641
+ const res = await requestText(`${convexUrl}/api/cli/refresh`, {
2642
+ method: "POST",
2643
+ headers: {
2644
+ Authorization: `Bearer ${currentToken}`,
2645
+ Connection: "close",
2646
+ "Content-Type": "application/json"
2647
+ }
2648
+ });
2649
+ if (res.status < 200 || res.status >= 300)
2650
+ return null;
2651
+ const body = JSON.parse(res.body);
2652
+ if (!body.token)
2653
+ return null;
2654
+ return { token: body.token, expiresAt: body.expiresAt ?? 0 };
2655
+ }
2656
+ function requestText(urlString, options) {
2657
+ const url = new URL(urlString);
2658
+ const request = url.protocol === "https:" ? httpsRequest : httpRequest;
2659
+ return new Promise((resolve4, reject) => {
2660
+ const req = request(url, {
2661
+ agent: false,
2662
+ headers: options.headers,
2663
+ method: options.method
2664
+ }, (res) => {
2665
+ let body = "";
2666
+ res.setEncoding("utf8");
2667
+ res.on("data", (chunk) => {
2668
+ body += chunk;
2669
+ });
2670
+ res.on("end", () => {
2671
+ resolve4({
2672
+ status: res.statusCode ?? 0,
2673
+ body
2674
+ });
2675
+ });
2676
+ res.on("error", reject);
2677
+ });
2678
+ req.on("error", reject);
2679
+ if (options.body) {
2680
+ req.write(options.body);
2681
+ }
2682
+ req.end();
2683
+ });
2684
+ }
2685
+ async function fetchDevices() {
2686
+ const { token, convexUrl } = getCloudConfig();
2687
+ const url = `${convexUrl}/api/cli/devices`;
2688
+ let activeToken = token;
2689
+ let res = await requestText(url, {
2690
+ method: "GET",
2691
+ headers: {
2692
+ Authorization: `Bearer ${activeToken}`,
2693
+ Connection: "close"
2694
+ }
2695
+ });
2696
+ if (res.status === 401) {
2697
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
2698
+ if (refreshed) {
2699
+ activeToken = refreshed;
2700
+ res = await requestText(url, {
2701
+ method: "GET",
2702
+ headers: {
2703
+ Authorization: `Bearer ${activeToken}`,
2704
+ Connection: "close"
2705
+ }
2706
+ });
2707
+ }
2708
+ }
2709
+ if (res.status < 200 || res.status >= 300) {
2710
+ return [];
2711
+ }
2712
+ const body = JSON.parse(res.body);
2713
+ return body.devices ?? [];
2714
+ }
2715
+
2477
2716
  // src/auth.ts
2478
2717
  import { spawn } from "node:child_process";
2479
2718
  import { createServer } from "node:http";
2480
- var DEFAULT_SITE_URL = "https://app.agendex.dev";
2719
+ var PROD_SITE_URL = "https://app.agendex.dev";
2720
+ var DEV_SITE_URL = "http://app.agendex.local:5174";
2721
+ function getDefaultSiteUrl() {
2722
+ if (process.env.AGENDEX_SITE_URL)
2723
+ return process.env.AGENDEX_SITE_URL;
2724
+ return process.env.AGENDEX_DEV === "1" ? DEV_SITE_URL : PROD_SITE_URL;
2725
+ }
2481
2726
  async function login(siteUrlOverride) {
2482
2727
  const { port, result } = await startCallbackServer();
2483
2728
  const callbackUrl = `http://127.0.0.1:${port}/callback`;
2484
- const siteUrl = siteUrlOverride ?? DEFAULT_SITE_URL;
2729
+ const siteUrl = siteUrlOverride ?? getDefaultSiteUrl();
2485
2730
  const authUrl = `${siteUrl}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
2486
2731
  console.log(`[agendex] Opening browser for authentication...`);
2487
2732
  console.log(`[agendex] If it doesn't open, visit: ${authUrl}`);
@@ -2666,185 +2911,6 @@ function spawnBrowser(command, args, options = {}) {
2666
2911
  import { spawn as spawn2 } from "node:child_process";
2667
2912
  import { resolve as resolve4 } from "node:path";
2668
2913
  import { fileURLToPath } from "node:url";
2669
-
2670
- // src/api.ts
2671
- import { request as httpRequest } from "node:http";
2672
- import { request as httpsRequest } from "node:https";
2673
- function getCloudConfig() {
2674
- const config = loadConfig();
2675
- if (!config?.cloudToken)
2676
- throw new Error("Not logged in. Run `agendex login` first.");
2677
- if (!config.convexUrl)
2678
- throw new Error("No Convex URL configured. Run `agendex login` first.");
2679
- return { token: config.cloudToken, convexUrl: config.convexUrl };
2680
- }
2681
- async function syncPlan(plan) {
2682
- const { token, convexUrl } = getCloudConfig();
2683
- const url = `${convexUrl}/api/cli/sync`;
2684
- let activeToken = token;
2685
- let res = await requestText(url, {
2686
- method: "POST",
2687
- headers: {
2688
- Authorization: `Bearer ${activeToken}`,
2689
- Connection: "close",
2690
- "Content-Type": "application/json"
2691
- },
2692
- body: JSON.stringify(plan)
2693
- });
2694
- if (res.status === 401) {
2695
- const refreshed = await refreshStoredToken(activeToken, convexUrl);
2696
- if (refreshed) {
2697
- activeToken = refreshed;
2698
- res = await requestText(url, {
2699
- method: "POST",
2700
- headers: {
2701
- Authorization: `Bearer ${activeToken}`,
2702
- Connection: "close",
2703
- "Content-Type": "application/json"
2704
- },
2705
- body: JSON.stringify(plan)
2706
- });
2707
- }
2708
- }
2709
- if (res.status < 200 || res.status >= 300) {
2710
- return { ok: false, error: `${res.status}: ${res.body}` };
2711
- }
2712
- return { ok: true };
2713
- }
2714
- async function refreshStoredToken(currentToken, convexUrl) {
2715
- const refreshed = await refreshToken(currentToken, convexUrl);
2716
- if (!refreshed)
2717
- return null;
2718
- const config = loadConfig();
2719
- if (config) {
2720
- saveConfig({ ...config, cloudToken: refreshed.token });
2721
- }
2722
- return refreshed.token;
2723
- }
2724
- async function sendHeartbeat() {
2725
- try {
2726
- const { token, convexUrl } = getCloudConfig();
2727
- let activeToken = token;
2728
- let res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2729
- method: "POST",
2730
- headers: {
2731
- Authorization: `Bearer ${activeToken}`,
2732
- Connection: "close",
2733
- "Content-Type": "application/json"
2734
- }
2735
- });
2736
- if (res.status === 401) {
2737
- const refreshedToken = await refreshStoredToken(activeToken, convexUrl);
2738
- if (!refreshedToken)
2739
- return;
2740
- activeToken = refreshedToken;
2741
- res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2742
- method: "POST",
2743
- headers: {
2744
- Authorization: `Bearer ${activeToken}`,
2745
- Connection: "close",
2746
- "Content-Type": "application/json"
2747
- }
2748
- });
2749
- }
2750
- if (res.status === 401) {
2751
- return;
2752
- }
2753
- } catch {}
2754
- }
2755
- async function refreshToken(currentToken, convexUrl) {
2756
- const res = await requestText(`${convexUrl}/api/cli/refresh`, {
2757
- method: "POST",
2758
- headers: {
2759
- Authorization: `Bearer ${currentToken}`,
2760
- Connection: "close",
2761
- "Content-Type": "application/json"
2762
- }
2763
- });
2764
- if (res.status < 200 || res.status >= 300)
2765
- return null;
2766
- const body = JSON.parse(res.body);
2767
- if (!body.token)
2768
- return null;
2769
- return { token: body.token, expiresAt: body.expiresAt ?? 0 };
2770
- }
2771
- function requestText(urlString, options) {
2772
- const url = new URL(urlString);
2773
- const request = url.protocol === "https:" ? httpsRequest : httpRequest;
2774
- return new Promise((resolve4, reject) => {
2775
- const req = request(url, {
2776
- agent: false,
2777
- headers: options.headers,
2778
- method: options.method
2779
- }, (res) => {
2780
- let body = "";
2781
- res.setEncoding("utf8");
2782
- res.on("data", (chunk) => {
2783
- body += chunk;
2784
- });
2785
- res.on("end", () => {
2786
- resolve4({
2787
- status: res.statusCode ?? 0,
2788
- body
2789
- });
2790
- });
2791
- res.on("error", reject);
2792
- });
2793
- req.on("error", reject);
2794
- if (options.body) {
2795
- req.write(options.body);
2796
- }
2797
- req.end();
2798
- });
2799
- }
2800
-
2801
- // src/pid.ts
2802
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2803
- import { homedir as homedir9, hostname } from "node:os";
2804
- import { dirname, join as join10 } from "node:path";
2805
- var pidPath = join10(homedir9(), ".agendex", "daemon.pid");
2806
- function writePid() {
2807
- mkdirSync2(dirname(pidPath), { recursive: true });
2808
- const info = {
2809
- pid: process.pid,
2810
- startedAtMs: Date.now(),
2811
- hostname: hostname()
2812
- };
2813
- writeFileSync2(pidPath, JSON.stringify(info));
2814
- }
2815
- function readPidInfo() {
2816
- if (!existsSync6(pidPath))
2817
- return null;
2818
- const raw = readFileSync3(pidPath, "utf-8").trim();
2819
- const asNumber = Number(raw);
2820
- if (Number.isFinite(asNumber) && asNumber > 0 && !raw.startsWith("{")) {
2821
- return { pid: asNumber };
2822
- }
2823
- try {
2824
- const parsed = JSON.parse(raw);
2825
- if (Number.isFinite(parsed.pid) && parsed.pid > 0)
2826
- return parsed;
2827
- } catch {}
2828
- return null;
2829
- }
2830
- function readPid() {
2831
- return readPidInfo()?.pid ?? null;
2832
- }
2833
- function removePid() {
2834
- try {
2835
- unlinkSync(pidPath);
2836
- } catch {}
2837
- }
2838
- function isRunning(pid) {
2839
- try {
2840
- process.kill(pid, 0);
2841
- return true;
2842
- } catch {
2843
- return false;
2844
- }
2845
- }
2846
-
2847
- // src/daemon.ts
2848
2914
  var MAX_RESTARTS = 5;
2849
2915
  var RESTART_WINDOW_MS = 60000;
2850
2916
  var RESTART_DELAY_MS = 5000;
@@ -3029,7 +3095,7 @@ import { join as join11 } from "node:path";
3029
3095
  // package.json
3030
3096
  var package_default = {
3031
3097
  name: "agendex-cli",
3032
- version: "0.5.0",
3098
+ version: "0.7.0",
3033
3099
  description: "Agendex CLI for login, sync, and daemon workflows",
3034
3100
  homepage: "https://github.com/Tyru5/Agendex#readme",
3035
3101
  repository: {
@@ -3240,6 +3306,28 @@ async function main() {
3240
3306
  writeStdout(`[agendex] Hostname: n/a`);
3241
3307
  }
3242
3308
  writeStdout(`[agendex] CLI version: ${CLI_VERSION}`);
3309
+ try {
3310
+ if (config?.cloudToken && config?.convexUrl) {
3311
+ const allDevices = await fetchDevices();
3312
+ if (allDevices.length > 0) {
3313
+ const now = Date.now();
3314
+ writeStdout(`[agendex] All daemons:`);
3315
+ for (const device of allDevices) {
3316
+ const age = device.lastSeenAt ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
3317
+ const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
3318
+ const uptimeStr = device.startedAtMs != null ? formatDuration(now - device.startedAtMs) : "~";
3319
+ const pidStr = device.pid != null ? String(device.pid) : "~";
3320
+ const hostnameStr = device.hostname ?? "~";
3321
+ writeStdout(`- hostname: ${hostnameStr}
3322
+ pid: ${pidStr}
3323
+ uptime: ${uptimeStr}
3324
+ status: ${status}`);
3325
+ }
3326
+ } else {
3327
+ writeStdout(`[agendex] All daemons: none`);
3328
+ }
3329
+ }
3330
+ } catch {}
3243
3331
  return 0;
3244
3332
  }
3245
3333
  case "help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {