agendex-cli 0.11.0 → 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 +308 -68
  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";
@@ -2853,7 +3019,7 @@ function requestText(urlString, options) {
2853
3019
  if (options.body) {
2854
3020
  headers["Content-Length"] = String(Buffer.byteLength(options.body));
2855
3021
  }
2856
- return new Promise((resolve4, reject) => {
3022
+ return new Promise((resolve5, reject) => {
2857
3023
  const req = request(url, {
2858
3024
  agent: false,
2859
3025
  headers,
@@ -2865,7 +3031,7 @@ function requestText(urlString, options) {
2865
3031
  body += chunk;
2866
3032
  });
2867
3033
  res.on("end", () => {
2868
- resolve4({
3034
+ resolve5({
2869
3035
  status: res.statusCode ?? 0,
2870
3036
  body
2871
3037
  });
@@ -2979,7 +3145,8 @@ async function login(siteUrlOverride) {
2979
3145
  token: existing?.token,
2980
3146
  cloudToken: callback.token,
2981
3147
  convexUrl: callback.convexUrl,
2982
- enabledAdapters: existing?.enabledAdapters ?? []
3148
+ enabledAdapters: existing?.enabledAdapters ?? [],
3149
+ customPlanDirs: existing?.customPlanDirs ?? []
2983
3150
  };
2984
3151
  saveConfig(config);
2985
3152
  console.log(`[agendex] Logged in successfully!`);
@@ -2996,7 +3163,8 @@ function logout() {
2996
3163
  token: existing.token,
2997
3164
  cloudToken: undefined,
2998
3165
  convexUrl: undefined,
2999
- enabledAdapters: existing.enabledAdapters
3166
+ enabledAdapters: existing.enabledAdapters,
3167
+ customPlanDirs: existing.customPlanDirs
3000
3168
  };
3001
3169
  saveConfig(config);
3002
3170
  console.log("[agendex] Logged out. Cloud token removed.");
@@ -3007,14 +3175,14 @@ async function startCallbackServer() {
3007
3175
  let timeout;
3008
3176
  let settle;
3009
3177
  let fail;
3010
- const result = new Promise((resolve4, reject) => {
3011
- settle = resolve4;
3178
+ const result = new Promise((resolve5, reject) => {
3179
+ settle = resolve5;
3012
3180
  fail = reject;
3013
3181
  });
3014
3182
  const finish = (value) => {
3015
3183
  if (!settle || !fail)
3016
3184
  return;
3017
- const resolve4 = settle;
3185
+ const resolve5 = settle;
3018
3186
  const reject = fail;
3019
3187
  settle = undefined;
3020
3188
  fail = undefined;
@@ -3031,7 +3199,7 @@ async function startCallbackServer() {
3031
3199
  reject(value);
3032
3200
  return;
3033
3201
  }
3034
- resolve4(value);
3202
+ resolve5(value);
3035
3203
  };
3036
3204
  server.on("request", (req, res) => {
3037
3205
  const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -3065,8 +3233,8 @@ async function startCallbackServer() {
3065
3233
  sockets.delete(socket);
3066
3234
  });
3067
3235
  });
3068
- await new Promise((resolve4, reject) => {
3069
- server.listen(0, "127.0.0.1", () => resolve4());
3236
+ await new Promise((resolve5, reject) => {
3237
+ server.listen(0, "127.0.0.1", () => resolve5());
3070
3238
  server.once("error", reject);
3071
3239
  });
3072
3240
  server.once("error", (error) => {
@@ -3146,7 +3314,7 @@ function spawnBrowser(command, args, options = {}) {
3146
3314
 
3147
3315
  // src/daemon.ts
3148
3316
  import { spawn as spawn2 } from "node:child_process";
3149
- import { resolve as resolve4 } from "node:path";
3317
+ import { resolve as resolve5 } from "node:path";
3150
3318
  import { fileURLToPath } from "node:url";
3151
3319
 
3152
3320
  // src/sync-cache.ts
@@ -3309,6 +3477,7 @@ async function runWorker() {
3309
3477
  });
3310
3478
  console.log(`[agendex] daemon running. Watching for file changes...`);
3311
3479
  async function gracefulShutdown() {
3480
+ stopWatching();
3312
3481
  await sendShutdown();
3313
3482
  process.exit(0);
3314
3483
  }
@@ -3332,17 +3501,17 @@ async function startSupervisor() {
3332
3501
  };
3333
3502
  process.on("SIGTERM", shutdown);
3334
3503
  process.on("SIGINT", shutdown);
3335
- 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)));
3336
3505
  const restartTimes = [];
3337
3506
  while (!stopping) {
3338
3507
  workerProc = spawn2(process.execPath, [scriptPath, "start", "--worker"], {
3339
3508
  stdio: ["ignore", "inherit", "inherit"]
3340
3509
  });
3341
- const exitCode = await new Promise((resolve5) => {
3342
- workerProc?.once("exit", (code) => resolve5(code));
3510
+ const exitCode = await new Promise((resolve6) => {
3511
+ workerProc?.once("exit", (code) => resolve6(code));
3343
3512
  workerProc?.once("error", (error) => {
3344
3513
  console.error("[agendex] failed to spawn worker:", error);
3345
- resolve5(1);
3514
+ resolve6(1);
3346
3515
  });
3347
3516
  });
3348
3517
  workerProc = null;
@@ -3421,7 +3590,7 @@ import { join as join12 } from "node:path";
3421
3590
  // package.json
3422
3591
  var package_default = {
3423
3592
  name: "agendex-cli",
3424
- version: "0.11.0",
3593
+ version: "0.12.0",
3425
3594
  description: "Agendex CLI for login, sync, and daemon workflows",
3426
3595
  homepage: "https://github.com/Tyru5/Agendex#readme",
3427
3596
  repository: {
@@ -3568,7 +3737,7 @@ function firstCommandToken(argv) {
3568
3737
  return;
3569
3738
  }
3570
3739
  var command = firstCommandToken(args) ?? "start";
3571
- var cliEntry = resolve5(process.argv[1] ?? fileURLToPath2(import.meta.url));
3740
+ var cliEntry = resolve6(process.argv[1] ?? fileURLToPath2(import.meta.url));
3572
3741
  async function main() {
3573
3742
  const isInternal = args.includes("--daemon") || args.includes("--worker");
3574
3743
  if (command === "--version" || command === "-v") {
@@ -3583,6 +3752,9 @@ async function main() {
3583
3752
  "open",
3584
3753
  "view",
3585
3754
  "cleanup",
3755
+ "add-dir",
3756
+ "remove-dir",
3757
+ "list-dirs",
3586
3758
  "help",
3587
3759
  "--help",
3588
3760
  "-h"
@@ -3758,6 +3930,66 @@ async function main() {
3758
3930
  }
3759
3931
  return 0;
3760
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
+ }
3761
3993
  case "status": {
3762
3994
  const config = loadConfig();
3763
3995
  const pidInfo = readPidInfo();
@@ -3768,6 +4000,11 @@ async function main() {
3768
4000
  writeStdout(`[agendex] Cloud token: ${config?.cloudToken ? "set" : "not set"}`);
3769
4001
  writeStdout(`[agendex] Convex URL: ${config?.convexUrl ?? "not set"}`);
3770
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
+ }
3771
4008
  writeStdout(`[agendex] Daemon: ${running ? `running (PID ${pid})` : "not running"}`);
3772
4009
  if (running && pidInfo?.startedAtMs) {
3773
4010
  writeStdout(`[agendex] Uptime: ${formatDuration(Date.now() - pidInfo.startedAtMs)}`);
@@ -3832,6 +4069,9 @@ Usage:
3832
4069
  agendex view <url> Open a shared plan URL in your browser
3833
4070
  agendex logout Clear stored cloud token
3834
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
3835
4075
  agendex sync One-shot scan + sync to cloud (skips unchanged plans)
3836
4076
  agendex sync --force Re-sync all plans, ignoring cache
3837
4077
  agendex cleanup Interactively remove cloud daemons
@@ -3865,13 +4105,13 @@ function flushStream(stream) {
3865
4105
  if (stream.destroyed || !stream.writable) {
3866
4106
  return Promise.resolve();
3867
4107
  }
3868
- return new Promise((resolve6, reject) => {
4108
+ return new Promise((resolve7, reject) => {
3869
4109
  stream.write("", (error) => {
3870
4110
  if (error) {
3871
4111
  reject(error);
3872
4112
  return;
3873
4113
  }
3874
- resolve6();
4114
+ resolve7();
3875
4115
  });
3876
4116
  });
3877
4117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.11.0",
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": {