agendex-cli 0.8.4 → 0.9.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 +29 -2
  2. package/dist/cli.js +165 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,6 +28,33 @@ agendex help # Show help message
28
28
  agendex --version / -v # Print CLI version
29
29
  ```
30
30
 
31
+ ## Dev vs prod (config directory)
32
+
33
+ By default the CLI uses **`~/.agendex/`** for all on-disk state:
34
+
35
+ - `config.json` — local token, cloud token, Convex URL, device id, enabled adapters
36
+ - `daemon.pid` — supervisor PID and metadata
37
+ - `sync-cache.json` — hashes used to skip unchanged plans on sync
38
+
39
+ To use a **separate dev environment** (so local cloud / dev login does not overwrite prod credentials), use either:
40
+
41
+ - **`--dev`** on any command (recommended), or
42
+ - **`AGENDEX_DEV=1`** in the environment
43
+
44
+ That switches the directory to **`~/.agendex-dev/`** with the same filenames inside it.
45
+
46
+ `--dev` takes precedence when set programmatically; otherwise `AGENDEX_DEV=1` is read. When you start the daemon with `agendex start --dev`, the background supervisor and worker inherit `AGENDEX_DEV=1` so they stay on the dev config.
47
+
48
+ Examples:
49
+
50
+ ```bash
51
+ agendex --dev login
52
+ agendex --dev status
53
+ AGENDEX_DEV=1 agendex sync
54
+ ```
55
+
56
+ In dev mode the default OAuth site (when you do not pass `--url` and do not set `AGENDEX_SITE_URL`) points at the local EE app URL used for development.
57
+
31
58
  ## Daemon Cleanup
32
59
 
33
60
  `agendex cleanup` manages registered daemon devices in the cloud.
@@ -76,6 +103,6 @@ For self-hosted deployments, pass your site URL explicitly:
76
103
  agendex login --url https://agendex.yourdomain.com
77
104
  ```
78
105
 
79
- This opens your deployment's OAuth flow and stores the returned `cloudToken` and `convexUrl` in `~/.agendex/config.json`.
106
+ This opens your deployment's OAuth flow and stores the returned `cloudToken` and `convexUrl` in your active config directory (`~/.agendex/config.json` for prod, `~/.agendex-dev/config.json` when using `--dev` or `AGENDEX_DEV=1`).
80
107
 
81
- The target can also be set via `AGENDEX_SITE_URL` env var. For local development, set `AGENDEX_DEV=1` to use the local dev server.
108
+ The target can also be set via `AGENDEX_SITE_URL` env var. For local development against the default dev app URL, use `agendex login --dev` or set `AGENDEX_DEV=1` (see [Dev vs prod](#dev-vs-prod-config-directory) above).
package/dist/cli.js CHANGED
@@ -2253,17 +2253,29 @@ async function promptForAdapterSelection(options = {}) {
2253
2253
  }
2254
2254
 
2255
2255
  // ../shared/src/config.ts
2256
- var configDir = join7(homedir7(), ".agendex");
2257
- var configPath = join7(configDir, "config.json");
2256
+ var devModeOverride;
2257
+ function setDevMode(dev) {
2258
+ devModeOverride = dev;
2259
+ }
2260
+ function isDevMode() {
2261
+ if (devModeOverride !== undefined)
2262
+ return devModeOverride;
2263
+ return process.env.AGENDEX_DEV === "1";
2264
+ }
2265
+ function getConfigDir() {
2266
+ return join7(homedir7(), isDevMode() ? ".agendex-dev" : ".agendex");
2267
+ }
2258
2268
  function ensureConfigDir() {
2259
- if (!existsSync3(configDir))
2260
- mkdirSync(configDir, { recursive: true });
2269
+ const dir = getConfigDir();
2270
+ if (!existsSync3(dir))
2271
+ mkdirSync(dir, { recursive: true });
2261
2272
  }
2262
2273
  function readStoredConfig() {
2263
- if (!existsSync3(configPath))
2274
+ const cfgPath = getConfigPath();
2275
+ if (!existsSync3(cfgPath))
2264
2276
  return null;
2265
2277
  try {
2266
- const raw = JSON.parse(readFileSync3(configPath, "utf-8"));
2278
+ const raw = JSON.parse(readFileSync3(cfgPath, "utf-8"));
2267
2279
  if (!raw || typeof raw !== "object")
2268
2280
  return null;
2269
2281
  return raw;
@@ -2305,7 +2317,7 @@ function saveConfig(config) {
2305
2317
  deviceId: config.deviceId,
2306
2318
  enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters)
2307
2319
  };
2308
- writeFileSync(configPath, JSON.stringify(payload, null, 2));
2320
+ writeFileSync(getConfigPath(), JSON.stringify(payload, null, 2));
2309
2321
  }
2310
2322
  function generateToken() {
2311
2323
  return randomBytes(32).toString("hex");
@@ -2324,7 +2336,7 @@ function loadOrCreateToken() {
2324
2336
  });
2325
2337
  console.log(`
2326
2338
  [agendex] generated auth token: ${token}`);
2327
- console.log(`[agendex] saved to ${configPath}
2339
+ console.log(`[agendex] saved to ${getConfigPath()}
2328
2340
  `);
2329
2341
  return token;
2330
2342
  }
@@ -2341,6 +2353,9 @@ function loadOrCreateDeviceId() {
2341
2353
  });
2342
2354
  return deviceId;
2343
2355
  }
2356
+ function getConfigPath() {
2357
+ return join7(getConfigDir(), "config.json");
2358
+ }
2344
2359
  async function loadOrInitConfig(options = {}) {
2345
2360
  const configureAdapters = Boolean(options.configureAdapters);
2346
2361
  const existing = loadConfig();
@@ -2389,7 +2404,9 @@ import { existsSync as existsSync4, readdirSync as readdirSync3, statSync } from
2389
2404
  import { lstat, mkdir, readdir as readdir2, readFile as readFile6, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
2390
2405
  import { homedir as homedir8 } from "node:os";
2391
2406
  import { join as join8, resolve as resolve2, sep as sep2 } from "node:path";
2392
- var USER_PLANS_DIR = join8(homedir8(), ".agendex", "plans");
2407
+ function getUserPlansDir() {
2408
+ return join8(getConfigDir(), "plans");
2409
+ }
2393
2410
  var store = new Map;
2394
2411
  var MAX_DEPTH = 6;
2395
2412
  var DISCOVERY_MAX_DEPTH = 4;
@@ -2496,9 +2513,10 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2496
2513
  return files;
2497
2514
  }
2498
2515
  async function scanUserPlans() {
2499
- if (!existsSync4(USER_PLANS_DIR))
2516
+ const userPlansDir = getUserPlansDir();
2517
+ if (!existsSync4(userPlansDir))
2500
2518
  return;
2501
- const files = await walkDir(USER_PLANS_DIR);
2519
+ const files = await walkDir(userPlansDir);
2502
2520
  for (const file of files) {
2503
2521
  if (!file.endsWith(".md"))
2504
2522
  continue;
@@ -2654,22 +2672,26 @@ import { hostname as osHostname } from "node:os";
2654
2672
 
2655
2673
  // src/pid.ts
2656
2674
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2657
- import { homedir as homedir9, hostname } from "node:os";
2675
+ import { hostname } from "node:os";
2658
2676
  import { dirname, join as join10 } from "node:path";
2659
- var pidPath = join10(homedir9(), ".agendex", "daemon.pid");
2677
+ function getPidPath() {
2678
+ return join10(getConfigDir(), "daemon.pid");
2679
+ }
2660
2680
  function writePid() {
2661
- mkdirSync2(dirname(pidPath), { recursive: true });
2681
+ const path = getPidPath();
2682
+ mkdirSync2(dirname(path), { recursive: true });
2662
2683
  const info = {
2663
2684
  pid: process.pid,
2664
2685
  startedAtMs: Date.now(),
2665
2686
  hostname: hostname()
2666
2687
  };
2667
- writeFileSync2(pidPath, JSON.stringify(info));
2688
+ writeFileSync2(path, JSON.stringify(info));
2668
2689
  }
2669
2690
  function readPidInfo() {
2670
- if (!existsSync6(pidPath))
2691
+ const path = getPidPath();
2692
+ if (!existsSync6(path))
2671
2693
  return null;
2672
- const raw = readFileSync4(pidPath, "utf-8").trim();
2694
+ const raw = readFileSync4(path, "utf-8").trim();
2673
2695
  const asNumber = Number(raw);
2674
2696
  if (Number.isFinite(asNumber) && asNumber > 0 && !raw.startsWith("{")) {
2675
2697
  return { pid: asNumber };
@@ -2686,7 +2708,7 @@ function readPid() {
2686
2708
  }
2687
2709
  function removePid() {
2688
2710
  try {
2689
- unlinkSync(pidPath);
2711
+ unlinkSync(getPidPath());
2690
2712
  } catch {}
2691
2713
  }
2692
2714
  function isRunning(pid) {
@@ -2706,7 +2728,10 @@ function getCloudConfig() {
2706
2728
  throw new Error("Not logged in. Run `agendex login` first.");
2707
2729
  if (!config.convexUrl)
2708
2730
  throw new Error("No Convex URL configured. Run `agendex login` first.");
2709
- return { token: config.cloudToken, convexUrl: config.convexUrl };
2731
+ return {
2732
+ token: config.cloudToken,
2733
+ convexUrl: config.convexUrl
2734
+ };
2710
2735
  }
2711
2736
  async function syncPlan(plan) {
2712
2737
  const { token, convexUrl } = getCloudConfig();
@@ -2914,7 +2939,7 @@ var DEV_SITE_URL = "http://app.agendex.local:5174";
2914
2939
  function getDefaultSiteUrl() {
2915
2940
  if (process.env.AGENDEX_SITE_URL)
2916
2941
  return process.env.AGENDEX_SITE_URL;
2917
- return process.env.AGENDEX_DEV === "1" ? DEV_SITE_URL : PROD_SITE_URL;
2942
+ return isDevMode() ? DEV_SITE_URL : PROD_SITE_URL;
2918
2943
  }
2919
2944
  async function login(siteUrlOverride) {
2920
2945
  const { port, result } = await startCallbackServer();
@@ -3104,6 +3129,56 @@ function spawnBrowser(command, args, options = {}) {
3104
3129
  import { spawn as spawn2 } from "node:child_process";
3105
3130
  import { resolve as resolve4 } from "node:path";
3106
3131
  import { fileURLToPath } from "node:url";
3132
+
3133
+ // src/sync-cache.ts
3134
+ import { createHash as createHash2 } from "node:crypto";
3135
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
3136
+ import { join as join11 } from "node:path";
3137
+ function getCachePath() {
3138
+ return join11(getConfigDir(), "sync-cache.json");
3139
+ }
3140
+ function loadSyncCache() {
3141
+ const cachePath = getCachePath();
3142
+ if (!existsSync7(cachePath))
3143
+ return {};
3144
+ try {
3145
+ const raw = JSON.parse(readFileSync5(cachePath, "utf-8"));
3146
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
3147
+ return {};
3148
+ return raw;
3149
+ } catch {
3150
+ return {};
3151
+ }
3152
+ }
3153
+ function saveSyncCache(cache, options) {
3154
+ const dir = getConfigDir();
3155
+ if (!existsSync7(dir))
3156
+ mkdirSync3(dir, { recursive: true });
3157
+ const cachePath = getCachePath();
3158
+ if (options?.replace) {
3159
+ writeFileSync3(cachePath, JSON.stringify(cache));
3160
+ return;
3161
+ }
3162
+ const existing = loadSyncCache();
3163
+ writeFileSync3(cachePath, JSON.stringify({ ...existing, ...cache }));
3164
+ }
3165
+ function computePayloadHash(payload) {
3166
+ const canonical = JSON.stringify([
3167
+ payload.localPlanId,
3168
+ payload.agent,
3169
+ payload.title,
3170
+ payload.content,
3171
+ payload.format,
3172
+ payload.filePath ?? null,
3173
+ payload.workspace ?? null,
3174
+ payload.metadata ?? null,
3175
+ payload.createdAt ?? null,
3176
+ payload.updatedAt ?? null
3177
+ ]);
3178
+ return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
3179
+ }
3180
+
3181
+ // src/daemon.ts
3107
3182
  var MAX_RESTARTS = 5;
3108
3183
  var RESTART_WINDOW_MS = 60000;
3109
3184
  var RESTART_DELAY_MS = 5000;
@@ -3127,6 +3202,7 @@ async function runWorker() {
3127
3202
  setActiveAdapters(adapters);
3128
3203
  console.log(`[agendex] daemon starting with ${config.enabledAdapters.length} adapters`);
3129
3204
  sendHeartbeat();
3205
+ const syncCache = loadSyncCache();
3130
3206
  const syncQueue = [];
3131
3207
  let syncing = false;
3132
3208
  async function tryRefreshToken() {
@@ -3167,6 +3243,7 @@ async function runWorker() {
3167
3243
  console.error(`[agendex] sync failed for "${payload.title}": ${result.error}`);
3168
3244
  } else {
3169
3245
  syncedCount++;
3246
+ syncCache[payload.localPlanId] = computePayloadHash(payload);
3170
3247
  }
3171
3248
  }
3172
3249
  } catch (err) {
@@ -3176,6 +3253,7 @@ async function runWorker() {
3176
3253
  syncing = false;
3177
3254
  }
3178
3255
  if (syncedCount > 0 || failedCount > 0) {
3256
+ saveSyncCache(syncCache);
3179
3257
  console.log(`[agendex] sync complete: ${syncedCount} synced, ${failedCount} failed`);
3180
3258
  }
3181
3259
  if (syncQueue.length > 0)
@@ -3185,10 +3263,23 @@ async function runWorker() {
3185
3263
  console.log(`[agendex] initial scan...`);
3186
3264
  await scan();
3187
3265
  const plans = getAll();
3188
- console.log(`[agendex] syncing ${plans.length} plans...`);
3266
+ let initialSkipped = 0;
3189
3267
  for (const plan of plans) {
3190
- syncQueue.push(planToPayload(plan));
3268
+ const payload = planToPayload(plan);
3269
+ const hash = computePayloadHash(payload);
3270
+ if (syncCache[plan.id] === hash) {
3271
+ initialSkipped++;
3272
+ continue;
3273
+ }
3274
+ syncQueue.push(payload);
3191
3275
  }
3276
+ const activePlanIds = new Set(plans.map((plan) => plan.id));
3277
+ for (const id of Object.keys(syncCache)) {
3278
+ if (!activePlanIds.has(id))
3279
+ delete syncCache[id];
3280
+ }
3281
+ saveSyncCache(syncCache, { replace: true });
3282
+ console.log(`[agendex] syncing ${syncQueue.length} plans (${initialSkipped} unchanged)...`);
3192
3283
  await processSyncQueue();
3193
3284
  setInterval(() => void sendHeartbeat(), CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
3194
3285
  startWatching((changedPlans) => {
@@ -3249,7 +3340,7 @@ async function startSupervisor() {
3249
3340
  }
3250
3341
 
3251
3342
  // src/sync.ts
3252
- async function syncAll() {
3343
+ async function syncAll(force = false) {
3253
3344
  const config = await loadOrInitConfig();
3254
3345
  const adapters = resolveAdapters(config.enabledAdapters);
3255
3346
  setActiveAdapters(adapters);
@@ -3257,9 +3348,13 @@ async function syncAll() {
3257
3348
  await scan();
3258
3349
  const plans = getAll();
3259
3350
  console.log(`[agendex] Found ${plans.length} plans. Syncing to cloud...`);
3351
+ const cache = force ? {} : loadSyncCache();
3352
+ const activePlanIds = new Set;
3260
3353
  let synced = 0;
3354
+ let skipped = 0;
3261
3355
  let failed = 0;
3262
3356
  for (const plan of plans) {
3357
+ activePlanIds.add(plan.id);
3263
3358
  const payload = {
3264
3359
  localPlanId: plan.id,
3265
3360
  agent: plan.agent,
@@ -3268,27 +3363,40 @@ async function syncAll() {
3268
3363
  format: plan.format,
3269
3364
  filePath: plan.filePath,
3270
3365
  workspace: plan.workspace,
3271
- metadata: plan.metadata
3366
+ metadata: plan.metadata,
3367
+ createdAt: plan.createdAt.getTime(),
3368
+ updatedAt: plan.updatedAt.getTime()
3272
3369
  };
3370
+ const hash = computePayloadHash(payload);
3371
+ if (!force && cache[plan.id] === hash) {
3372
+ skipped++;
3373
+ continue;
3374
+ }
3273
3375
  const result = await syncPlan(payload);
3274
3376
  if (result.ok) {
3275
3377
  synced++;
3378
+ cache[plan.id] = hash;
3276
3379
  } else {
3277
3380
  failed++;
3278
3381
  console.error(`[agendex] Failed to sync "${plan.title}": ${result.error}`);
3279
3382
  }
3280
3383
  }
3281
- console.log(`[agendex] Sync complete: ${synced} synced, ${failed} failed`);
3384
+ for (const id of Object.keys(cache)) {
3385
+ if (!activePlanIds.has(id))
3386
+ delete cache[id];
3387
+ }
3388
+ saveSyncCache(cache, { replace: true });
3389
+ console.log(`[agendex] Sync complete: ${synced} synced, ${skipped} unchanged, ${failed} failed`);
3282
3390
  }
3283
3391
 
3284
3392
  // src/version.ts
3285
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
3393
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
3286
3394
  import { tmpdir } from "node:os";
3287
- import { join as join11 } from "node:path";
3395
+ import { join as join12 } from "node:path";
3288
3396
  // package.json
3289
3397
  var package_default = {
3290
3398
  name: "agendex-cli",
3291
- version: "0.8.4",
3399
+ version: "0.9.0",
3292
3400
  description: "Agendex CLI for login, sync, and daemon workflows",
3293
3401
  homepage: "https://github.com/Tyru5/Agendex#readme",
3294
3402
  repository: {
@@ -3332,14 +3440,14 @@ var package_default = {
3332
3440
 
3333
3441
  // src/version.ts
3334
3442
  var CLI_VERSION = package_default.version;
3335
- var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join11(tmpdir(), ".agendex-update-cache.json");
3443
+ var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join12(tmpdir(), ".agendex-update-cache.json");
3336
3444
  var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
3337
3445
  var UPDATE_URL = process.env.AGENDEX_UPDATE_URL ?? "https://registry.npmjs.org/agendex-cli/latest";
3338
3446
  function readCache(current) {
3339
3447
  try {
3340
- if (!existsSync7(CACHE_FILE))
3448
+ if (!existsSync8(CACHE_FILE))
3341
3449
  return null;
3342
- const { result, ts } = JSON.parse(readFileSync5(CACHE_FILE, "utf8"));
3450
+ const { result, ts } = JSON.parse(readFileSync6(CACHE_FILE, "utf8"));
3343
3451
  if (Date.now() - ts > CACHE_TTL_MS)
3344
3452
  return null;
3345
3453
  return normalizeResult(result, current);
@@ -3349,7 +3457,7 @@ function readCache(current) {
3349
3457
  }
3350
3458
  function writeCache(result) {
3351
3459
  try {
3352
- writeFileSync3(CACHE_FILE, JSON.stringify({ result, ts: Date.now() }));
3460
+ writeFileSync4(CACHE_FILE, JSON.stringify({ result, ts: Date.now() }));
3353
3461
  } catch {}
3354
3462
  }
3355
3463
  async function checkForUpdate() {
@@ -3400,7 +3508,18 @@ function isNewer(latest, current) {
3400
3508
 
3401
3509
  // src/cli.ts
3402
3510
  var args = process.argv.slice(2);
3403
- var command = args[0] ?? "start";
3511
+ var devFlag = args.includes("--dev");
3512
+ if (devFlag)
3513
+ setDevMode(true);
3514
+ function firstCommandToken(argv) {
3515
+ for (const a of argv) {
3516
+ if (a === "--dev")
3517
+ continue;
3518
+ return a;
3519
+ }
3520
+ return;
3521
+ }
3522
+ var command = firstCommandToken(args) ?? "start";
3404
3523
  var cliEntry = resolve5(process.argv[1] ?? fileURLToPath2(import.meta.url));
3405
3524
  async function main() {
3406
3525
  const isInternal = args.includes("--daemon") || args.includes("--worker");
@@ -3443,9 +3562,13 @@ async function main() {
3443
3562
  }
3444
3563
  if (existingPid)
3445
3564
  removePid();
3446
- const child = spawn3(process.execPath, [cliEntry, "start", "--daemon"], {
3565
+ const daemonArgs = [cliEntry, "start", "--daemon"];
3566
+ if (devFlag)
3567
+ daemonArgs.push("--dev");
3568
+ const child = spawn3(process.execPath, daemonArgs, {
3447
3569
  detached: true,
3448
- stdio: "ignore"
3570
+ stdio: "ignore",
3571
+ env: { ...process.env, ...devFlag ? { AGENDEX_DEV: "1" } : {} }
3449
3572
  });
3450
3573
  child.unref();
3451
3574
  await new Promise((r) => setTimeout(r, 500));
@@ -3489,7 +3612,8 @@ async function main() {
3489
3612
  return 0;
3490
3613
  }
3491
3614
  case "sync": {
3492
- await syncAll();
3615
+ const force = args.includes("--force");
3616
+ await syncAll(force);
3493
3617
  return 0;
3494
3618
  }
3495
3619
  case "cleanup": {
@@ -3622,13 +3746,17 @@ Usage:
3622
3746
  agendex login --url <url> Login to a self-hosted instance
3623
3747
  agendex logout Clear stored cloud token
3624
3748
  agendex configure Select which agents/adapters to index
3625
- agendex sync One-shot scan + sync to cloud
3749
+ agendex sync One-shot scan + sync to cloud (skips unchanged plans)
3750
+ agendex sync --force Re-sync all plans, ignoring cache
3626
3751
  agendex cleanup Interactively remove cloud daemons
3627
3752
  agendex cleanup --stale Auto-remove all stale daemons
3628
3753
  agendex status Show current config state + daemon status
3629
3754
  agendex help Show this help message
3630
3755
  agendex --version Print CLI version
3631
3756
  agendex -v Print CLI version
3757
+
3758
+ Flags:
3759
+ --dev Use dev environment (~/.agendex-dev/ config dir)
3632
3760
  `.trim());
3633
3761
  return 0;
3634
3762
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {