agendex-cli 0.4.0 → 0.6.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/README.md +32 -8
  2. package/dist/cli.js +123 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,15 +14,39 @@ bun install -g agendex-cli
14
14
  ## Commands
15
15
 
16
16
  ```bash
17
- agendex login
18
- agendex login --url https://agendex.yourdomain.com
19
- agendex configure
20
- agendex start
21
- agendex stop
22
- agendex sync
23
- agendex status
24
- agendex logout
17
+ agendex login # Authenticate via browser OAuth (agendex.dev)
18
+ agendex login --url <url> # Login to a self-hosted instance
19
+ agendex logout # Clear stored cloud token
20
+ agendex configure # Select which agents/adapters to index
21
+ agendex start # Start daemon (backgrounds itself)
22
+ agendex stop # Stop the running daemon
23
+ agendex sync # One-shot scan + sync to cloud
24
+ agendex status # Show config state, daemon status, uptime & hostname
25
+ agendex help # Show help message
26
+ agendex --version / -v # Print CLI version
27
+ ```
28
+
29
+ ## Status Output
30
+
31
+ `agendex status` prints a rich overview:
32
+
33
+ - Config version, local/cloud token state, Convex URL
34
+ - Enabled adapters
35
+ - Daemon running state with PID
36
+ - **Uptime** — how long the daemon has been running
37
+ - **Hostname** — machine the daemon is running on
38
+ - CLI version
39
+
40
+ ## Auto-Update Check
41
+
42
+ Before running `start`, `configure`, or `sync`, the CLI checks for a newer published version. If an update is required the command is blocked and you are prompted to upgrade:
43
+
25
44
  ```
45
+ [agendex] update required: v0.1.0 → v0.2.0
46
+ [agendex] run: npm i -g agendex-cli
47
+ ```
48
+
49
+ The check is skipped for `stop`, `status`, `login`, `logout`, and `help`.
26
50
 
27
51
  ## Supported Runtime
28
52
 
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);
@@ -2477,11 +2495,17 @@ function startWatching(onChange) {
2477
2495
  // src/auth.ts
2478
2496
  import { spawn } from "node:child_process";
2479
2497
  import { createServer } from "node:http";
2480
- var DEFAULT_SITE_URL = "https://app.agendex.dev";
2498
+ var PROD_SITE_URL = "https://app.agendex.dev";
2499
+ var DEV_SITE_URL = "http://app.agendex.local:5174";
2500
+ function getDefaultSiteUrl() {
2501
+ if (process.env.AGENDEX_SITE_URL)
2502
+ return process.env.AGENDEX_SITE_URL;
2503
+ return process.env.AGENDEX_DEV === "1" ? DEV_SITE_URL : PROD_SITE_URL;
2504
+ }
2481
2505
  async function login(siteUrlOverride) {
2482
2506
  const { port, result } = await startCallbackServer();
2483
2507
  const callbackUrl = `http://127.0.0.1:${port}/callback`;
2484
- const siteUrl = siteUrlOverride ?? DEFAULT_SITE_URL;
2508
+ const siteUrl = siteUrlOverride ?? getDefaultSiteUrl();
2485
2509
  const authUrl = `${siteUrl}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
2486
2510
  console.log(`[agendex] Opening browser for authentication...`);
2487
2511
  console.log(`[agendex] If it doesn't open, visit: ${authUrl}`);
@@ -2670,6 +2694,56 @@ import { fileURLToPath } from "node:url";
2670
2694
  // src/api.ts
2671
2695
  import { request as httpRequest } from "node:http";
2672
2696
  import { request as httpsRequest } from "node:https";
2697
+ import { hostname as osHostname } from "node:os";
2698
+
2699
+ // src/pid.ts
2700
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2701
+ import { homedir as homedir9, hostname } from "node:os";
2702
+ import { dirname, join as join10 } from "node:path";
2703
+ var pidPath = join10(homedir9(), ".agendex", "daemon.pid");
2704
+ function writePid() {
2705
+ mkdirSync2(dirname(pidPath), { recursive: true });
2706
+ const info = {
2707
+ pid: process.pid,
2708
+ startedAtMs: Date.now(),
2709
+ hostname: hostname()
2710
+ };
2711
+ writeFileSync2(pidPath, JSON.stringify(info));
2712
+ }
2713
+ function readPidInfo() {
2714
+ if (!existsSync6(pidPath))
2715
+ return null;
2716
+ const raw = readFileSync3(pidPath, "utf-8").trim();
2717
+ const asNumber = Number(raw);
2718
+ if (Number.isFinite(asNumber) && asNumber > 0 && !raw.startsWith("{")) {
2719
+ return { pid: asNumber };
2720
+ }
2721
+ try {
2722
+ const parsed = JSON.parse(raw);
2723
+ if (Number.isFinite(parsed.pid) && parsed.pid > 0)
2724
+ return parsed;
2725
+ } catch {}
2726
+ return null;
2727
+ }
2728
+ function readPid() {
2729
+ return readPidInfo()?.pid ?? null;
2730
+ }
2731
+ function removePid() {
2732
+ try {
2733
+ unlinkSync(pidPath);
2734
+ } catch {}
2735
+ }
2736
+ function isRunning(pid) {
2737
+ try {
2738
+ process.kill(pid, 0);
2739
+ return true;
2740
+ } catch {
2741
+ return false;
2742
+ }
2743
+ }
2744
+
2745
+ // src/api.ts
2746
+ var cachedDeviceId;
2673
2747
  function getCloudConfig() {
2674
2748
  const config = loadConfig();
2675
2749
  if (!config?.cloudToken)
@@ -2724,6 +2798,12 @@ async function refreshStoredToken(currentToken, convexUrl) {
2724
2798
  async function sendHeartbeat() {
2725
2799
  try {
2726
2800
  const { token, convexUrl } = getCloudConfig();
2801
+ const pidInfo = readPidInfo();
2802
+ const heartbeatBody = JSON.stringify({
2803
+ deviceId: cachedDeviceId ??= loadOrCreateDeviceId(),
2804
+ hostname: pidInfo?.hostname ?? osHostname(),
2805
+ startedAtMs: pidInfo?.startedAtMs
2806
+ });
2727
2807
  let activeToken = token;
2728
2808
  let res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2729
2809
  method: "POST",
@@ -2731,7 +2811,8 @@ async function sendHeartbeat() {
2731
2811
  Authorization: `Bearer ${activeToken}`,
2732
2812
  Connection: "close",
2733
2813
  "Content-Type": "application/json"
2734
- }
2814
+ },
2815
+ body: heartbeatBody
2735
2816
  });
2736
2817
  if (res.status === 401) {
2737
2818
  const refreshedToken = await refreshStoredToken(activeToken, convexUrl);
@@ -2744,7 +2825,8 @@ async function sendHeartbeat() {
2744
2825
  Authorization: `Bearer ${activeToken}`,
2745
2826
  Connection: "close",
2746
2827
  "Content-Type": "application/json"
2747
- }
2828
+ },
2829
+ body: heartbeatBody
2748
2830
  });
2749
2831
  }
2750
2832
  if (res.status === 401) {
@@ -2798,36 +2880,6 @@ function requestText(urlString, options) {
2798
2880
  });
2799
2881
  }
2800
2882
 
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 } 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
- writeFileSync2(pidPath, String(process.pid));
2809
- }
2810
- function readPid() {
2811
- if (!existsSync6(pidPath))
2812
- return null;
2813
- const raw = readFileSync3(pidPath, "utf-8").trim();
2814
- const pid = Number(raw);
2815
- return Number.isFinite(pid) && pid > 0 ? pid : null;
2816
- }
2817
- function removePid() {
2818
- try {
2819
- unlinkSync(pidPath);
2820
- } catch {}
2821
- }
2822
- function isRunning(pid) {
2823
- try {
2824
- process.kill(pid, 0);
2825
- return true;
2826
- } catch {
2827
- return false;
2828
- }
2829
- }
2830
-
2831
2883
  // src/daemon.ts
2832
2884
  var MAX_RESTARTS = 5;
2833
2885
  var RESTART_WINDOW_MS = 60000;
@@ -3013,7 +3065,7 @@ import { join as join11 } from "node:path";
3013
3065
  // package.json
3014
3066
  var package_default = {
3015
3067
  name: "agendex-cli",
3016
- version: "0.4.0",
3068
+ version: "0.6.0",
3017
3069
  description: "Agendex CLI for login, sync, and daemon workflows",
3018
3070
  homepage: "https://github.com/Tyru5/Agendex#readme",
3019
3071
  repository: {
@@ -3119,6 +3171,10 @@ var command = args[0] ?? "start";
3119
3171
  var cliEntry = resolve5(process.argv[1] ?? fileURLToPath2(import.meta.url));
3120
3172
  async function main() {
3121
3173
  const isInternal = args.includes("--daemon") || args.includes("--worker");
3174
+ if (command === "--version" || command === "-v") {
3175
+ writeStdout(CLI_VERSION);
3176
+ return 0;
3177
+ }
3122
3178
  const isPassthrough = ["stop", "status", "login", "logout", "help", "--help", "-h"].includes(command);
3123
3179
  if (!isInternal && !isPassthrough) {
3124
3180
  const { updateAvailable, current, latest } = await checkForUpdate();
@@ -3196,7 +3252,8 @@ async function main() {
3196
3252
  }
3197
3253
  case "status": {
3198
3254
  const config = loadConfig();
3199
- const pid = readPid();
3255
+ const pidInfo = readPidInfo();
3256
+ const pid = pidInfo?.pid ?? null;
3200
3257
  const running = pid ? isRunning(pid) : false;
3201
3258
  writeStdout(`[agendex] Config version: ${config?.configVersion ?? "none"}`);
3202
3259
  writeStdout(`[agendex] Local token: ${config?.token ? "set" : "not set"}`);
@@ -3204,6 +3261,20 @@ async function main() {
3204
3261
  writeStdout(`[agendex] Convex URL: ${config?.convexUrl ?? "not set"}`);
3205
3262
  writeStdout(`[agendex] Enabled adapters: ${config?.enabledAdapters.join(", ") || "none"}`);
3206
3263
  writeStdout(`[agendex] Daemon: ${running ? `running (PID ${pid})` : "not running"}`);
3264
+ if (running && pidInfo?.startedAtMs) {
3265
+ writeStdout(`[agendex] Uptime: ${formatDuration(Date.now() - pidInfo.startedAtMs)}`);
3266
+ } else if (running) {
3267
+ writeStdout(`[agendex] Uptime: unknown (restart daemon to populate)`);
3268
+ } else {
3269
+ writeStdout(`[agendex] Uptime: n/a`);
3270
+ }
3271
+ if (running && pidInfo?.hostname) {
3272
+ writeStdout(`[agendex] Hostname: ${pidInfo.hostname}`);
3273
+ } else if (running) {
3274
+ writeStdout(`[agendex] Hostname: unknown (restart daemon to populate)`);
3275
+ } else {
3276
+ writeStdout(`[agendex] Hostname: n/a`);
3277
+ }
3207
3278
  writeStdout(`[agendex] CLI version: ${CLI_VERSION}`);
3208
3279
  return 0;
3209
3280
  }
@@ -3224,6 +3295,8 @@ Usage:
3224
3295
  agendex sync One-shot scan + sync to cloud
3225
3296
  agendex status Show current config state + daemon status
3226
3297
  agendex help Show this help message
3298
+ agendex --version Print CLI version
3299
+ agendex -v Print CLI version
3227
3300
  `.trim());
3228
3301
  return 0;
3229
3302
  }
@@ -3264,3 +3337,17 @@ function writeStderr(message) {
3264
3337
  writeSync(process.stderr.fd, `${message}
3265
3338
  `);
3266
3339
  }
3340
+ function formatDuration(ms) {
3341
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
3342
+ const days = Math.floor(totalSeconds / 86400);
3343
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
3344
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
3345
+ const seconds = totalSeconds % 60;
3346
+ if (days > 0)
3347
+ return `${days}d ${hours}h ${minutes}m ${seconds}s`;
3348
+ if (hours > 0)
3349
+ return `${hours}h ${minutes}m ${seconds}s`;
3350
+ if (minutes > 0)
3351
+ return `${minutes}m ${seconds}s`;
3352
+ return `${seconds}s`;
3353
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {