agendex-cli 0.10.1 → 0.12.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 +333 -70
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1057,8 +1057,8 @@ var init_cleanup = __esm(() => {
1057
1057
 
1058
1058
  // src/cli.ts
1059
1059
  import { spawn as spawn3 } from "node:child_process";
1060
- import { writeSync } from "node:fs";
1061
- import { resolve as resolve5 } from "node:path";
1060
+ import { existsSync as existsSync9, statSync as statSync2, writeSync } from "node:fs";
1061
+ import { resolve as resolve6 } from "node:path";
1062
1062
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1063
1063
 
1064
1064
  // ../shared/src/adapters/catalog.ts
@@ -2198,7 +2198,7 @@ var activeAdapters = resolveAdapters(getDefaultAdapterIds());
2198
2198
  import { randomBytes } from "node:crypto";
2199
2199
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2200
2200
  import { homedir as homedir7 } from "node:os";
2201
- import { join as join7 } from "node:path";
2201
+ import { join as join7, resolve as resolve2 } from "node:path";
2202
2202
 
2203
2203
  // ../shared/src/setup/adapter-selection.ts
2204
2204
  init_dist2();
@@ -2288,6 +2288,37 @@ function normalizeAdapterIds(input) {
2288
2288
  return [];
2289
2289
  return sanitizeEnabledAdapterIds(input.filter((item) => typeof item === "string"));
2290
2290
  }
2291
+ function expandHomePath(p) {
2292
+ if (p.startsWith("~/") || p === "~")
2293
+ return join7(homedir7(), p.slice(1));
2294
+ return p;
2295
+ }
2296
+ function resolveCustomPlanDirPath(userPath) {
2297
+ const trimmed = userPath.trim();
2298
+ if (!trimmed) {
2299
+ throw new Error("Custom plan directory path must not be empty");
2300
+ }
2301
+ return resolve2(expandHomePath(trimmed));
2302
+ }
2303
+ function normalizeCustomPlanDirs(input) {
2304
+ if (!Array.isArray(input))
2305
+ return [];
2306
+ const seen = new Set;
2307
+ const result = [];
2308
+ for (const item of input) {
2309
+ if (typeof item !== "string")
2310
+ continue;
2311
+ const trimmed = item.trim();
2312
+ if (!trimmed)
2313
+ continue;
2314
+ const normalized = resolveCustomPlanDirPath(trimmed);
2315
+ if (seen.has(normalized))
2316
+ continue;
2317
+ seen.add(normalized);
2318
+ result.push(normalized);
2319
+ }
2320
+ return result;
2321
+ }
2291
2322
  function normalizeStoredConfig(raw) {
2292
2323
  if (!raw)
2293
2324
  return null;
@@ -2301,7 +2332,8 @@ function normalizeStoredConfig(raw) {
2301
2332
  cloudToken,
2302
2333
  convexUrl,
2303
2334
  deviceId,
2304
- enabledAdapters: normalizeAdapterIds(raw.enabledAdapters)
2335
+ enabledAdapters: normalizeAdapterIds(raw.enabledAdapters),
2336
+ customPlanDirs: normalizeCustomPlanDirs(raw.customPlanDirs)
2305
2337
  };
2306
2338
  }
2307
2339
  function loadConfig() {
@@ -2315,7 +2347,8 @@ function saveConfig(config) {
2315
2347
  cloudToken: config.cloudToken,
2316
2348
  convexUrl: config.convexUrl,
2317
2349
  deviceId: config.deviceId,
2318
- enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters)
2350
+ enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters),
2351
+ customPlanDirs: normalizeCustomPlanDirs(config.customPlanDirs)
2319
2352
  };
2320
2353
  writeFileSync(getConfigPath(), JSON.stringify(payload, null, 2));
2321
2354
  }
@@ -2332,7 +2365,8 @@ function loadOrCreateToken() {
2332
2365
  saveConfig({
2333
2366
  configVersion: 3,
2334
2367
  token,
2335
- enabledAdapters: existing?.enabledAdapters ?? []
2368
+ enabledAdapters: existing?.enabledAdapters ?? [],
2369
+ customPlanDirs: existing?.customPlanDirs ?? []
2336
2370
  });
2337
2371
  console.log(`
2338
2372
  [agendex] generated auth token: ${token}`);
@@ -2349,7 +2383,8 @@ function loadOrCreateDeviceId() {
2349
2383
  ...existing ?? {},
2350
2384
  configVersion: 3,
2351
2385
  deviceId,
2352
- enabledAdapters: existing?.enabledAdapters ?? []
2386
+ enabledAdapters: existing?.enabledAdapters ?? [],
2387
+ customPlanDirs: existing?.customPlanDirs ?? []
2353
2388
  });
2354
2389
  return deviceId;
2355
2390
  }
@@ -2388,7 +2423,8 @@ async function loadOrInitConfig(options = {}) {
2388
2423
  cloudToken: existing?.cloudToken,
2389
2424
  convexUrl: existing?.convexUrl,
2390
2425
  deviceId,
2391
- enabledAdapters
2426
+ enabledAdapters,
2427
+ customPlanDirs: existing?.customPlanDirs ?? []
2392
2428
  };
2393
2429
  saveConfig(nextConfig);
2394
2430
  return {
@@ -2403,7 +2439,7 @@ var CLI_DAEMON_STALE_AFTER_MS = CLI_DAEMON_HEARTBEAT_INTERVAL_MS * 5;
2403
2439
  import { existsSync as existsSync4, readdirSync as readdirSync3, statSync } from "node:fs";
2404
2440
  import { lstat, mkdir, readdir as readdir2, readFile as readFile6, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
2405
2441
  import { homedir as homedir8 } from "node:os";
2406
- import { join as join8, resolve as resolve2, sep as sep2 } from "node:path";
2442
+ import { join as join8, resolve as resolve3, sep as sep2 } from "node:path";
2407
2443
  function getUserPlansDir() {
2408
2444
  return join8(getConfigDir(), "plans");
2409
2445
  }
@@ -2489,7 +2525,7 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2489
2525
  return [];
2490
2526
  if (!existsSync4(dir))
2491
2527
  return [];
2492
- const real = resolve2(dir);
2528
+ const real = resolve3(dir);
2493
2529
  if (seen.has(real))
2494
2530
  return [];
2495
2531
  seen.add(real);
@@ -2512,7 +2548,36 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2512
2548
  } catch {}
2513
2549
  return files;
2514
2550
  }
2515
- async function scanUserPlans() {
2551
+ async function parseGenericMarkdownPlan(filePath, extraMetadata) {
2552
+ try {
2553
+ const content = await readFile6(filePath, "utf-8");
2554
+ const stats = await stat6(filePath);
2555
+ let agent = "unknown";
2556
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
2557
+ if (fmMatch) {
2558
+ const agentLine = fmMatch[1]?.match(/^agent:\s*(.+)$/m);
2559
+ if (agentLine?.[1])
2560
+ agent = agentLine[1].trim();
2561
+ }
2562
+ const bodyContent = fmMatch ? content.slice(fmMatch[0].length) : content;
2563
+ const titleMatch = bodyContent.match(/^#\s+(.+)/m);
2564
+ const title = titleMatch?.[1]?.trim() || filePath.split("/").pop()?.replace(".md", "") || "Untitled";
2565
+ return {
2566
+ id: hashPath(filePath),
2567
+ agent,
2568
+ title,
2569
+ content: bodyContent,
2570
+ filePath,
2571
+ format: "md",
2572
+ createdAt: stats.birthtime,
2573
+ updatedAt: stats.mtime,
2574
+ metadata: extraMetadata
2575
+ };
2576
+ } catch {
2577
+ return null;
2578
+ }
2579
+ }
2580
+ async function scanUserPlans(into) {
2516
2581
  const userPlansDir = getUserPlansDir();
2517
2582
  if (!existsSync4(userPlansDir))
2518
2583
  return;
@@ -2520,55 +2585,89 @@ async function scanUserPlans() {
2520
2585
  for (const file of files) {
2521
2586
  if (!file.endsWith(".md"))
2522
2587
  continue;
2523
- try {
2524
- const content = await readFile6(file, "utf-8");
2525
- const stats = await stat6(file);
2526
- let agent = "unknown";
2527
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
2528
- if (fmMatch) {
2529
- const agentLine = fmMatch[1]?.match(/^agent:\s*(.+)$/m);
2530
- if (agentLine?.[1])
2531
- agent = agentLine[1].trim();
2588
+ const plan = await parseGenericMarkdownPlan(file, { userCreated: true });
2589
+ if (plan)
2590
+ into.set(plan.id, plan);
2591
+ }
2592
+ }
2593
+ function getCustomPlanDirs() {
2594
+ return loadConfig()?.customPlanDirs ?? [];
2595
+ }
2596
+ function pathsOverlapFilesystemTree(a, b) {
2597
+ const ra = resolve3(a);
2598
+ const rb = resolve3(b);
2599
+ if (ra === rb)
2600
+ return true;
2601
+ if (ra.startsWith(rb + sep2))
2602
+ return true;
2603
+ if (rb.startsWith(ra + sep2))
2604
+ return true;
2605
+ return false;
2606
+ }
2607
+ function overlapsAnyRoot(candidate, roots) {
2608
+ const resolvedCandidate = resolve3(candidate);
2609
+ for (const root of roots) {
2610
+ if (pathsOverlapFilesystemTree(resolvedCandidate, root))
2611
+ return true;
2612
+ }
2613
+ return false;
2614
+ }
2615
+ async function scanCustomPlanDirs(coveredPaths, into) {
2616
+ const dirs = getCustomPlanDirs();
2617
+ const userPlansDir = resolve3(getUserPlansDir());
2618
+ for (const dir of dirs) {
2619
+ const resolved = resolve3(dir);
2620
+ if (overlapsAnyRoot(resolved, coveredPaths)) {
2621
+ console.log(`[agendex] skipping custom dir (overlaps adapter / discovered coverage): ${dir}`);
2622
+ continue;
2623
+ }
2624
+ if (pathsOverlapFilesystemTree(resolved, userPlansDir)) {
2625
+ console.log(`[agendex] skipping custom dir (overlaps user plans): ${dir}`);
2626
+ continue;
2627
+ }
2628
+ if (!existsSync4(dir)) {
2629
+ console.log(`[agendex] skipping custom dir (not found): ${dir}`);
2630
+ continue;
2631
+ }
2632
+ const files = await walkDir(dir);
2633
+ let count = 0;
2634
+ for (const file of files) {
2635
+ if (!file.endsWith(".md"))
2636
+ continue;
2637
+ const plan = await parseGenericMarkdownPlan(file, {
2638
+ source: "custom-dir",
2639
+ customDir: dir
2640
+ });
2641
+ if (plan) {
2642
+ into.set(plan.id, plan);
2643
+ count++;
2532
2644
  }
2533
- const bodyContent = fmMatch ? content.slice(fmMatch[0].length) : content;
2534
- const titleMatch = bodyContent.match(/^#\s+(.+)/m);
2535
- const title = titleMatch?.[1]?.trim() || file.split("/").pop()?.replace(".md", "") || "Untitled";
2536
- const plan = {
2537
- id: hashPath(file),
2538
- agent,
2539
- title,
2540
- content: bodyContent,
2541
- filePath: file,
2542
- format: "md",
2543
- createdAt: stats.birthtime,
2544
- updatedAt: stats.mtime,
2545
- metadata: { userCreated: true }
2546
- };
2547
- store.set(plan.id, plan);
2548
- } catch {}
2645
+ }
2646
+ console.log(`[agendex] custom dir: ${dir} (${count} plans)`);
2549
2647
  }
2550
2648
  }
2551
2649
  async function scan() {
2552
2650
  const adapters = getActiveAdapters();
2553
- store.clear();
2651
+ const next = new Map;
2554
2652
  const coveredPaths = new Set;
2555
2653
  for (const adapter of adapters) {
2556
2654
  for (const searchPath of adapter.getSearchPaths()) {
2557
- coveredPaths.add(resolve2(searchPath));
2655
+ coveredPaths.add(resolve3(searchPath));
2558
2656
  const files = await walkDir(searchPath);
2559
2657
  for (const file of files) {
2560
2658
  if (!adapter.matches(file))
2561
2659
  continue;
2562
2660
  const plans = await adapter.parse(file);
2563
2661
  for (const plan of plans) {
2564
- store.set(plan.id, plan);
2662
+ next.set(plan.id, plan);
2565
2663
  }
2566
2664
  }
2567
2665
  }
2568
2666
  }
2569
2667
  const discovered = discoverProjectPlanDirs();
2570
2668
  for (const { dir, agent } of discovered) {
2571
- if (coveredPaths.has(resolve2(dir)))
2669
+ const resolvedDir = resolve3(dir);
2670
+ if (coveredPaths.has(resolvedDir))
2572
2671
  continue;
2573
2672
  const adapter = adapters.find((a) => a.agent === agent);
2574
2673
  if (!adapter)
@@ -2579,12 +2678,15 @@ async function scan() {
2579
2678
  continue;
2580
2679
  const plans = await adapter.parse(file);
2581
2680
  for (const plan of plans) {
2582
- store.set(plan.id, plan);
2681
+ next.set(plan.id, plan);
2583
2682
  }
2584
2683
  }
2684
+ coveredPaths.add(resolvedDir);
2585
2685
  console.log(`[agendex] discovered project plans: ${dir}`);
2586
2686
  }
2587
- await scanUserPlans();
2687
+ await scanUserPlans(next);
2688
+ await scanCustomPlanDirs(coveredPaths, next);
2689
+ store = next;
2588
2690
  notifyPlansChanged();
2589
2691
  console.log(`[agendex] indexed ${store.size} plans from ${adapters.length} adapters`);
2590
2692
  }
@@ -2593,13 +2695,13 @@ function getAll() {
2593
2695
  }
2594
2696
  async function rescanFile(filePath) {
2595
2697
  const adapters = getActiveAdapters();
2596
- const normalized = resolve2(filePath);
2698
+ const normalized = resolve3(filePath);
2597
2699
  for (const adapter of adapters) {
2598
2700
  if (!adapter.matches(filePath))
2599
2701
  continue;
2600
- const discoveredDirs = discoverProjectPlanDirs().filter((d2) => d2.agent === adapter.agent).map((d2) => resolve2(d2.dir));
2702
+ const discoveredDirs = discoverProjectPlanDirs().filter((d2) => d2.agent === adapter.agent).map((d2) => resolve3(d2.dir));
2601
2703
  const allSearchPaths = [
2602
- ...adapter.getSearchPaths().map((sp) => resolve2(sp)),
2704
+ ...adapter.getSearchPaths().map((sp) => resolve3(sp)),
2603
2705
  ...discoveredDirs
2604
2706
  ];
2605
2707
  const isInSearchPath = allSearchPaths.some((sp) => normalized.startsWith(sp + sep2) || normalized === sp);
@@ -2612,18 +2714,58 @@ async function rescanFile(filePath) {
2612
2714
  notifyPlansChanged();
2613
2715
  return plans;
2614
2716
  }
2717
+ const userPlansDir = resolve3(getUserPlansDir());
2718
+ if (normalized.endsWith(".md") && (normalized.startsWith(userPlansDir + sep2) || normalized === userPlansDir)) {
2719
+ const plan = await parseGenericMarkdownPlan(filePath, { userCreated: true });
2720
+ if (plan) {
2721
+ store.set(plan.id, plan);
2722
+ notifyPlansChanged();
2723
+ return [plan];
2724
+ }
2725
+ }
2726
+ if (normalized.endsWith(".md")) {
2727
+ const customDirs = getCustomPlanDirs();
2728
+ for (const dir of customDirs) {
2729
+ const resolvedDir = resolve3(dir);
2730
+ if (normalized.startsWith(resolvedDir + sep2) || normalized === resolvedDir) {
2731
+ const plan = await parseGenericMarkdownPlan(filePath, {
2732
+ source: "custom-dir",
2733
+ customDir: dir
2734
+ });
2735
+ if (plan) {
2736
+ store.set(plan.id, plan);
2737
+ notifyPlansChanged();
2738
+ return [plan];
2739
+ }
2740
+ }
2741
+ }
2742
+ }
2615
2743
  return [];
2616
2744
  }
2617
2745
  // ../shared/src/services/watcher.ts
2618
2746
  import { existsSync as existsSync5, watch } from "node:fs";
2619
- import { join as join9, resolve as resolve3 } from "node:path";
2747
+ import { join as join9, resolve as resolve4 } from "node:path";
2620
2748
  var debounceTimer = null;
2621
2749
  var pendingFiles = new Set;
2750
+ var activeWatchers = [];
2751
+ function closeAllWatchers() {
2752
+ if (debounceTimer) {
2753
+ clearTimeout(debounceTimer);
2754
+ debounceTimer = null;
2755
+ }
2756
+ pendingFiles.clear();
2757
+ for (const watcher of activeWatchers) {
2758
+ try {
2759
+ watcher.close();
2760
+ } catch {}
2761
+ }
2762
+ activeWatchers.length = 0;
2763
+ }
2622
2764
  function watchDir(dir, matchFn, onChange) {
2623
2765
  if (!existsSync5(dir))
2624
2766
  return;
2625
2767
  try {
2626
- watch(dir, { recursive: true }, async (_event, filename) => {
2768
+ const watcher = watch(dir, { recursive: true }, async (_event, filename) => {
2627
2769
  if (!filename)
2628
2770
  return;
2629
2771
  const fullPath = join9(dir, filename);
@@ -2643,27 +2785,51 @@ function watchDir(dir, matchFn, onChange) {
2643
2785
  onChange?.(allPlans);
2644
2786
  }, 300);
2645
2787
  });
2788
+ activeWatchers.push(watcher);
2646
2789
  console.log(`[agendex] watching ${dir}`);
2647
2790
  } catch {}
2648
2791
  }
2649
2792
  function startWatching(onChange) {
2793
+ closeAllWatchers();
2794
+ setupWatchers(onChange);
2795
+ }
2796
+ function stopWatching() {
2797
+ closeAllWatchers();
2798
+ }
2799
+ function setupWatchers(onChange) {
2650
2800
  const adapters = getActiveAdapters();
2651
2801
  const watchedPaths = new Set;
2652
2802
  for (const adapter of adapters) {
2653
2803
  for (const watchPath of adapter.getWatchPaths()) {
2654
- watchedPaths.add(resolve3(watchPath));
2804
+ watchedPaths.add(resolve4(watchPath));
2655
2805
  watchDir(watchPath, (f) => adapter.matches(f), onChange);
2656
2806
  }
2657
2807
  }
2658
2808
  const discovered = discoverProjectPlanDirs();
2659
2809
  for (const { dir, agent } of discovered) {
2660
- if (watchedPaths.has(resolve3(dir)))
2810
+ if (watchedPaths.has(resolve4(dir)))
2661
2811
  continue;
2662
2812
  const adapter = adapters.find((a) => a.agent === agent);
2663
2813
  if (!adapter)
2664
2814
  continue;
2815
+ watchedPaths.add(resolve4(dir));
2665
2816
  watchDir(dir, (f) => adapter.matches(f), onChange);
2666
2817
  }
2818
+ const customDirs = getCustomPlanDirs();
2819
+ for (const dir of customDirs) {
2820
+ const resolvedCustom = resolve4(dir);
2821
+ let overlaps = false;
2822
+ for (const watched of watchedPaths) {
2823
+ if (pathsOverlapFilesystemTree(resolvedCustom, watched)) {
2824
+ overlaps = true;
2825
+ break;
2826
+ }
2827
+ }
2828
+ if (overlaps)
2829
+ continue;
2830
+ watchedPaths.add(resolvedCustom);
2831
+ watchDir(dir, (f) => f.endsWith(".md"), onChange);
2832
+ }
2667
2833
  }
2668
2834
  // src/api.ts
2669
2835
  import { request as httpRequest } from "node:http";
@@ -2721,6 +2887,12 @@ function isRunning(pid) {
2721
2887
  }
2722
2888
 
2723
2889
  // src/api.ts
2890
+ class AuthExpiredError extends Error {
2891
+ constructor() {
2892
+ super("Cloud token expired. Run `agendex login` to re-authenticate.");
2893
+ this.name = "AuthExpiredError";
2894
+ }
2895
+ }
2724
2896
  var cachedDeviceId;
2725
2897
  function getCloudConfig() {
2726
2898
  const config = loadConfig();
@@ -2847,7 +3019,7 @@ function requestText(urlString, options) {
2847
3019
  if (options.body) {
2848
3020
  headers["Content-Length"] = String(Buffer.byteLength(options.body));
2849
3021
  }
2850
- return new Promise((resolve4, reject) => {
3022
+ return new Promise((resolve5, reject) => {
2851
3023
  const req = request(url, {
2852
3024
  agent: false,
2853
3025
  headers,
@@ -2859,7 +3031,7 @@ function requestText(urlString, options) {
2859
3031
  body += chunk;
2860
3032
  });
2861
3033
  res.on("end", () => {
2862
- resolve4({
3034
+ resolve5({
2863
3035
  status: res.statusCode ?? 0,
2864
3036
  body
2865
3037
  });
@@ -2897,6 +3069,9 @@ async function fetchDevices() {
2897
3069
  });
2898
3070
  }
2899
3071
  }
3072
+ if (res.status === 401) {
3073
+ throw new AuthExpiredError;
3074
+ }
2900
3075
  if (res.status < 200 || res.status >= 300) {
2901
3076
  return [];
2902
3077
  }
@@ -2970,7 +3145,8 @@ async function login(siteUrlOverride) {
2970
3145
  token: existing?.token,
2971
3146
  cloudToken: callback.token,
2972
3147
  convexUrl: callback.convexUrl,
2973
- enabledAdapters: existing?.enabledAdapters ?? []
3148
+ enabledAdapters: existing?.enabledAdapters ?? [],
3149
+ customPlanDirs: existing?.customPlanDirs ?? []
2974
3150
  };
2975
3151
  saveConfig(config);
2976
3152
  console.log(`[agendex] Logged in successfully!`);
@@ -2987,7 +3163,8 @@ function logout() {
2987
3163
  token: existing.token,
2988
3164
  cloudToken: undefined,
2989
3165
  convexUrl: undefined,
2990
- enabledAdapters: existing.enabledAdapters
3166
+ enabledAdapters: existing.enabledAdapters,
3167
+ customPlanDirs: existing.customPlanDirs
2991
3168
  };
2992
3169
  saveConfig(config);
2993
3170
  console.log("[agendex] Logged out. Cloud token removed.");
@@ -2998,14 +3175,14 @@ async function startCallbackServer() {
2998
3175
  let timeout;
2999
3176
  let settle;
3000
3177
  let fail;
3001
- const result = new Promise((resolve4, reject) => {
3002
- settle = resolve4;
3178
+ const result = new Promise((resolve5, reject) => {
3179
+ settle = resolve5;
3003
3180
  fail = reject;
3004
3181
  });
3005
3182
  const finish = (value) => {
3006
3183
  if (!settle || !fail)
3007
3184
  return;
3008
- const resolve4 = settle;
3185
+ const resolve5 = settle;
3009
3186
  const reject = fail;
3010
3187
  settle = undefined;
3011
3188
  fail = undefined;
@@ -3022,7 +3199,7 @@ async function startCallbackServer() {
3022
3199
  reject(value);
3023
3200
  return;
3024
3201
  }
3025
- resolve4(value);
3202
+ resolve5(value);
3026
3203
  };
3027
3204
  server.on("request", (req, res) => {
3028
3205
  const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -3056,8 +3233,8 @@ async function startCallbackServer() {
3056
3233
  sockets.delete(socket);
3057
3234
  });
3058
3235
  });
3059
- await new Promise((resolve4, reject) => {
3060
- server.listen(0, "127.0.0.1", () => resolve4());
3236
+ await new Promise((resolve5, reject) => {
3237
+ server.listen(0, "127.0.0.1", () => resolve5());
3061
3238
  server.once("error", reject);
3062
3239
  });
3063
3240
  server.once("error", (error) => {
@@ -3137,7 +3314,7 @@ function spawnBrowser(command, args, options = {}) {
3137
3314
 
3138
3315
  // src/daemon.ts
3139
3316
  import { spawn as spawn2 } from "node:child_process";
3140
- import { resolve as resolve4 } from "node:path";
3317
+ import { resolve as resolve5 } from "node:path";
3141
3318
  import { fileURLToPath } from "node:url";
3142
3319
 
3143
3320
  // src/sync-cache.ts
@@ -3300,6 +3477,7 @@ async function runWorker() {
3300
3477
  });
3301
3478
  console.log(`[agendex] daemon running. Watching for file changes...`);
3302
3479
  async function gracefulShutdown() {
3480
+ stopWatching();
3303
3481
  await sendShutdown();
3304
3482
  process.exit(0);
3305
3483
  }
@@ -3323,17 +3501,17 @@ async function startSupervisor() {
3323
3501
  };
3324
3502
  process.on("SIGTERM", shutdown);
3325
3503
  process.on("SIGINT", shutdown);
3326
- const scriptPath = resolve4(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
3504
+ const scriptPath = resolve5(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
3327
3505
  const restartTimes = [];
3328
3506
  while (!stopping) {
3329
3507
  workerProc = spawn2(process.execPath, [scriptPath, "start", "--worker"], {
3330
3508
  stdio: ["ignore", "inherit", "inherit"]
3331
3509
  });
3332
- const exitCode = await new Promise((resolve5) => {
3333
- workerProc?.once("exit", (code) => resolve5(code));
3510
+ const exitCode = await new Promise((resolve6) => {
3511
+ workerProc?.once("exit", (code) => resolve6(code));
3334
3512
  workerProc?.once("error", (error) => {
3335
3513
  console.error("[agendex] failed to spawn worker:", error);
3336
- resolve5(1);
3514
+ resolve6(1);
3337
3515
  });
3338
3516
  });
3339
3517
  workerProc = null;
@@ -3412,7 +3590,7 @@ import { join as join12 } from "node:path";
3412
3590
  // package.json
3413
3591
  var package_default = {
3414
3592
  name: "agendex-cli",
3415
- version: "0.10.1",
3593
+ version: "0.12.0",
3416
3594
  description: "Agendex CLI for login, sync, and daemon workflows",
3417
3595
  homepage: "https://github.com/Tyru5/Agendex#readme",
3418
3596
  repository: {
@@ -3559,7 +3737,7 @@ function firstCommandToken(argv) {
3559
3737
  return;
3560
3738
  }
3561
3739
  var command = firstCommandToken(args) ?? "start";
3562
- var cliEntry = resolve5(process.argv[1] ?? fileURLToPath2(import.meta.url));
3740
+ var cliEntry = resolve6(process.argv[1] ?? fileURLToPath2(import.meta.url));
3563
3741
  async function main() {
3564
3742
  const isInternal = args.includes("--daemon") || args.includes("--worker");
3565
3743
  if (command === "--version" || command === "-v") {
@@ -3574,6 +3752,9 @@ async function main() {
3574
3752
  "open",
3575
3753
  "view",
3576
3754
  "cleanup",
3755
+ "add-dir",
3756
+ "remove-dir",
3757
+ "list-dirs",
3577
3758
  "help",
3578
3759
  "--help",
3579
3760
  "-h"
@@ -3680,7 +3861,16 @@ async function main() {
3680
3861
  writeStderr("[agendex] not logged in. Run `agendex login` first.");
3681
3862
  return 1;
3682
3863
  }
3683
- const allDevices = await fetchDevices();
3864
+ let allDevices;
3865
+ try {
3866
+ allDevices = await fetchDevices();
3867
+ } catch (err) {
3868
+ if (err instanceof AuthExpiredError) {
3869
+ writeStderr("[agendex] cloud token expired. Run `agendex login` to re-authenticate.");
3870
+ return 1;
3871
+ }
3872
+ throw err;
3873
+ }
3684
3874
  if (allDevices.length === 0) {
3685
3875
  writeStdout("[agendex] no daemons found");
3686
3876
  return 0;
@@ -3740,6 +3930,66 @@ async function main() {
3740
3930
  }
3741
3931
  return 0;
3742
3932
  }
3933
+ case "add-dir": {
3934
+ const dirPath = args.find((a) => a !== "add-dir" && a !== "--dev" && !a.startsWith("--"));
3935
+ if (!dirPath || !dirPath.trim()) {
3936
+ writeStderr("[agendex] usage: agendex add-dir <path>");
3937
+ return 1;
3938
+ }
3939
+ const resolved = resolveCustomPlanDirPath(dirPath);
3940
+ if (!existsSync9(resolved)) {
3941
+ writeStderr(`[agendex] path does not exist: ${resolved}`);
3942
+ return 1;
3943
+ }
3944
+ if (!statSync2(resolved).isDirectory()) {
3945
+ writeStderr(`[agendex] path is not a directory: ${resolved}`);
3946
+ return 1;
3947
+ }
3948
+ const cfg = loadConfig();
3949
+ const currentDirs = cfg?.customPlanDirs ?? [];
3950
+ const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
3951
+ saveConfig({
3952
+ ...cfg ?? { configVersion: 3, enabledAdapters: [] },
3953
+ customPlanDirs: updated
3954
+ });
3955
+ writeStdout(`[agendex] added custom plan dir: ${resolved}`);
3956
+ writeStdout(`[agendex] daemon will pick up the change automatically`);
3957
+ return 0;
3958
+ }
3959
+ case "remove-dir": {
3960
+ const dirPath = args.find((a) => a !== "remove-dir" && a !== "--dev" && !a.startsWith("--"));
3961
+ if (!dirPath || !dirPath.trim()) {
3962
+ writeStderr("[agendex] usage: agendex remove-dir <path>");
3963
+ return 1;
3964
+ }
3965
+ const resolved = resolveCustomPlanDirPath(dirPath);
3966
+ const cfg = loadConfig();
3967
+ const currentDirs = cfg?.customPlanDirs ?? [];
3968
+ const updated = currentDirs.filter((d2) => d2 !== resolved);
3969
+ if (updated.length === currentDirs.length) {
3970
+ writeStderr(`[agendex] directory not in custom plan dirs: ${resolved}`);
3971
+ return 1;
3972
+ }
3973
+ saveConfig({
3974
+ ...cfg ?? { configVersion: 3, enabledAdapters: [] },
3975
+ customPlanDirs: updated
3976
+ });
3977
+ writeStdout(`[agendex] removed custom plan dir: ${resolved}`);
3978
+ return 0;
3979
+ }
3980
+ case "list-dirs": {
3981
+ const cfg = loadConfig();
3982
+ const dirs = cfg?.customPlanDirs ?? [];
3983
+ if (dirs.length === 0) {
3984
+ writeStdout("[agendex] no custom plan directories configured");
3985
+ } else {
3986
+ writeStdout(`[agendex] custom plan directories (${dirs.length}):`);
3987
+ for (const dir of dirs) {
3988
+ writeStdout(` - ${dir}`);
3989
+ }
3990
+ }
3991
+ return 0;
3992
+ }
3743
3993
  case "status": {
3744
3994
  const config = loadConfig();
3745
3995
  const pidInfo = readPidInfo();
@@ -3750,6 +4000,11 @@ async function main() {
3750
4000
  writeStdout(`[agendex] Cloud token: ${config?.cloudToken ? "set" : "not set"}`);
3751
4001
  writeStdout(`[agendex] Convex URL: ${config?.convexUrl ?? "not set"}`);
3752
4002
  writeStdout(`[agendex] Enabled adapters: ${config?.enabledAdapters.join(", ") || "none"}`);
4003
+ const customDirs = config?.customPlanDirs ?? [];
4004
+ writeStdout(`[agendex] Custom plan dirs: ${customDirs.length > 0 ? customDirs.length : "none"}`);
4005
+ for (const dir of customDirs) {
4006
+ writeStdout(` - ${dir}`);
4007
+ }
3753
4008
  writeStdout(`[agendex] Daemon: ${running ? `running (PID ${pid})` : "not running"}`);
3754
4009
  if (running && pidInfo?.startedAtMs) {
3755
4010
  writeStdout(`[agendex] Uptime: ${formatDuration(Date.now() - pidInfo.startedAtMs)}`);
@@ -3789,7 +4044,12 @@ async function main() {
3789
4044
  writeStdout(`[agendex] All daemons: none`);
3790
4045
  }
3791
4046
  }
3792
- } catch {}
4047
+ } catch (err) {
4048
+ if (err instanceof AuthExpiredError) {
4049
+ writeStderr(`[agendex] Cloud token expired — cloud sync and daemon tracking are inactive.`);
4050
+ writeStderr(`[agendex] Run \`agendex login\` to re-authenticate.`);
4051
+ }
4052
+ }
3793
4053
  return 0;
3794
4054
  }
3795
4055
  case "help":
@@ -3809,6 +4069,9 @@ Usage:
3809
4069
  agendex view <url> Open a shared plan URL in your browser
3810
4070
  agendex logout Clear stored cloud token
3811
4071
  agendex configure Select which agents/adapters to index
4072
+ agendex add-dir <path> Add a custom directory to scan for plans
4073
+ agendex remove-dir <path> Remove a custom directory
4074
+ agendex list-dirs List custom plan directories
3812
4075
  agendex sync One-shot scan + sync to cloud (skips unchanged plans)
3813
4076
  agendex sync --force Re-sync all plans, ignoring cache
3814
4077
  agendex cleanup Interactively remove cloud daemons
@@ -3842,13 +4105,13 @@ function flushStream(stream) {
3842
4105
  if (stream.destroyed || !stream.writable) {
3843
4106
  return Promise.resolve();
3844
4107
  }
3845
- return new Promise((resolve6, reject) => {
4108
+ return new Promise((resolve7, reject) => {
3846
4109
  stream.write("", (error) => {
3847
4110
  if (error) {
3848
4111
  reject(error);
3849
4112
  return;
3850
4113
  }
3851
- resolve6();
4114
+ resolve7();
3852
4115
  });
3853
4116
  });
3854
4117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {