agendex-cli 0.11.0 → 0.13.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 +359 -68
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  var __create = Object.create;
3
4
  var __getProtoOf = Object.getPrototypeOf;
4
5
  var __defProp = Object.defineProperty;
@@ -44,6 +45,7 @@ var __export = (target, all) => {
44
45
  });
45
46
  };
46
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
47
49
 
48
50
  // ../../node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
49
51
  var require_picocolors = __commonJS((exports, module) => {
@@ -1057,8 +1059,8 @@ var init_cleanup = __esm(() => {
1057
1059
 
1058
1060
  // src/cli.ts
1059
1061
  import { spawn as spawn3 } from "node:child_process";
1060
- import { writeSync } from "node:fs";
1061
- import { resolve as resolve5 } from "node:path";
1062
+ import { existsSync as existsSync9, statSync as statSync2, writeSync } from "node:fs";
1063
+ import { resolve as resolve6 } from "node:path";
1062
1064
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1063
1065
 
1064
1066
  // ../shared/src/adapters/catalog.ts
@@ -2198,7 +2200,7 @@ var activeAdapters = resolveAdapters(getDefaultAdapterIds());
2198
2200
  import { randomBytes } from "node:crypto";
2199
2201
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2200
2202
  import { homedir as homedir7 } from "node:os";
2201
- import { join as join7 } from "node:path";
2203
+ import { join as join7, resolve as resolve2 } from "node:path";
2202
2204
 
2203
2205
  // ../shared/src/setup/adapter-selection.ts
2204
2206
  init_dist2();
@@ -2288,6 +2290,37 @@ function normalizeAdapterIds(input) {
2288
2290
  return [];
2289
2291
  return sanitizeEnabledAdapterIds(input.filter((item) => typeof item === "string"));
2290
2292
  }
2293
+ function expandHomePath(p) {
2294
+ if (p.startsWith("~/") || p === "~")
2295
+ return join7(homedir7(), p.slice(1));
2296
+ return p;
2297
+ }
2298
+ function resolveCustomPlanDirPath(userPath) {
2299
+ const trimmed = userPath.trim();
2300
+ if (!trimmed) {
2301
+ throw new Error("Custom plan directory path must not be empty");
2302
+ }
2303
+ return resolve2(expandHomePath(trimmed));
2304
+ }
2305
+ function normalizeCustomPlanDirs(input) {
2306
+ if (!Array.isArray(input))
2307
+ return [];
2308
+ const seen = new Set;
2309
+ const result = [];
2310
+ for (const item of input) {
2311
+ if (typeof item !== "string")
2312
+ continue;
2313
+ const trimmed = item.trim();
2314
+ if (!trimmed)
2315
+ continue;
2316
+ const normalized = resolveCustomPlanDirPath(trimmed);
2317
+ if (seen.has(normalized))
2318
+ continue;
2319
+ seen.add(normalized);
2320
+ result.push(normalized);
2321
+ }
2322
+ return result;
2323
+ }
2291
2324
  function normalizeStoredConfig(raw) {
2292
2325
  if (!raw)
2293
2326
  return null;
@@ -2301,7 +2334,8 @@ function normalizeStoredConfig(raw) {
2301
2334
  cloudToken,
2302
2335
  convexUrl,
2303
2336
  deviceId,
2304
- enabledAdapters: normalizeAdapterIds(raw.enabledAdapters)
2337
+ enabledAdapters: normalizeAdapterIds(raw.enabledAdapters),
2338
+ customPlanDirs: normalizeCustomPlanDirs(raw.customPlanDirs)
2305
2339
  };
2306
2340
  }
2307
2341
  function loadConfig() {
@@ -2315,7 +2349,8 @@ function saveConfig(config) {
2315
2349
  cloudToken: config.cloudToken,
2316
2350
  convexUrl: config.convexUrl,
2317
2351
  deviceId: config.deviceId,
2318
- enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters)
2352
+ enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters),
2353
+ customPlanDirs: normalizeCustomPlanDirs(config.customPlanDirs)
2319
2354
  };
2320
2355
  writeFileSync(getConfigPath(), JSON.stringify(payload, null, 2));
2321
2356
  }
@@ -2332,7 +2367,8 @@ function loadOrCreateToken() {
2332
2367
  saveConfig({
2333
2368
  configVersion: 3,
2334
2369
  token,
2335
- enabledAdapters: existing?.enabledAdapters ?? []
2370
+ enabledAdapters: existing?.enabledAdapters ?? [],
2371
+ customPlanDirs: existing?.customPlanDirs ?? []
2336
2372
  });
2337
2373
  console.log(`
2338
2374
  [agendex] generated auth token: ${token}`);
@@ -2349,7 +2385,8 @@ function loadOrCreateDeviceId() {
2349
2385
  ...existing ?? {},
2350
2386
  configVersion: 3,
2351
2387
  deviceId,
2352
- enabledAdapters: existing?.enabledAdapters ?? []
2388
+ enabledAdapters: existing?.enabledAdapters ?? [],
2389
+ customPlanDirs: existing?.customPlanDirs ?? []
2353
2390
  });
2354
2391
  return deviceId;
2355
2392
  }
@@ -2388,7 +2425,8 @@ async function loadOrInitConfig(options = {}) {
2388
2425
  cloudToken: existing?.cloudToken,
2389
2426
  convexUrl: existing?.convexUrl,
2390
2427
  deviceId,
2391
- enabledAdapters
2428
+ enabledAdapters,
2429
+ customPlanDirs: existing?.customPlanDirs ?? []
2392
2430
  };
2393
2431
  saveConfig(nextConfig);
2394
2432
  return {
@@ -2403,7 +2441,7 @@ var CLI_DAEMON_STALE_AFTER_MS = CLI_DAEMON_HEARTBEAT_INTERVAL_MS * 5;
2403
2441
  import { existsSync as existsSync4, readdirSync as readdirSync3, statSync } from "node:fs";
2404
2442
  import { lstat, mkdir, readdir as readdir2, readFile as readFile6, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
2405
2443
  import { homedir as homedir8 } from "node:os";
2406
- import { join as join8, resolve as resolve2, sep as sep2 } from "node:path";
2444
+ import { join as join8, resolve as resolve3, sep as sep2 } from "node:path";
2407
2445
  function getUserPlansDir() {
2408
2446
  return join8(getConfigDir(), "plans");
2409
2447
  }
@@ -2489,7 +2527,7 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2489
2527
  return [];
2490
2528
  if (!existsSync4(dir))
2491
2529
  return [];
2492
- const real = resolve2(dir);
2530
+ const real = resolve3(dir);
2493
2531
  if (seen.has(real))
2494
2532
  return [];
2495
2533
  seen.add(real);
@@ -2512,7 +2550,36 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2512
2550
  } catch {}
2513
2551
  return files;
2514
2552
  }
2515
- async function scanUserPlans() {
2553
+ async function parseGenericMarkdownPlan(filePath, extraMetadata) {
2554
+ try {
2555
+ const content = await readFile6(filePath, "utf-8");
2556
+ const stats = await stat6(filePath);
2557
+ let agent = (typeof extraMetadata.agentHint === "string" ? extraMetadata.agentHint : "") || "unknown";
2558
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
2559
+ if (fmMatch) {
2560
+ const agentLine = fmMatch[1]?.match(/^agent:\s*(.+)$/m);
2561
+ if (agentLine?.[1])
2562
+ agent = agentLine[1].trim();
2563
+ }
2564
+ const bodyContent = fmMatch ? content.slice(fmMatch[0].length) : content;
2565
+ const titleMatch = bodyContent.match(/^#\s+(.+)/m);
2566
+ const title = titleMatch?.[1]?.trim() || filePath.split("/").pop()?.replace(".md", "") || "Untitled";
2567
+ return {
2568
+ id: hashPath(filePath),
2569
+ agent,
2570
+ title,
2571
+ content: bodyContent,
2572
+ filePath,
2573
+ format: "md",
2574
+ createdAt: stats.birthtime,
2575
+ updatedAt: stats.mtime,
2576
+ metadata: extraMetadata
2577
+ };
2578
+ } catch {
2579
+ return null;
2580
+ }
2581
+ }
2582
+ async function scanUserPlans(into) {
2516
2583
  const userPlansDir = getUserPlansDir();
2517
2584
  if (!existsSync4(userPlansDir))
2518
2585
  return;
@@ -2520,55 +2587,91 @@ async function scanUserPlans() {
2520
2587
  for (const file of files) {
2521
2588
  if (!file.endsWith(".md"))
2522
2589
  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();
2590
+ const plan = await parseGenericMarkdownPlan(file, { userCreated: true });
2591
+ if (plan)
2592
+ into.set(plan.id, plan);
2593
+ }
2594
+ }
2595
+ function getCustomPlanDirs() {
2596
+ return loadConfig()?.customPlanDirs ?? [];
2597
+ }
2598
+ function pathsOverlapFilesystemTree(a, b) {
2599
+ const ra = resolve3(a);
2600
+ const rb = resolve3(b);
2601
+ if (ra === rb)
2602
+ return true;
2603
+ if (ra.startsWith(rb + sep2))
2604
+ return true;
2605
+ if (rb.startsWith(ra + sep2))
2606
+ return true;
2607
+ return false;
2608
+ }
2609
+ function overlapsAnyRoot(candidate, roots) {
2610
+ const resolvedCandidate = resolve3(candidate);
2611
+ for (const root of roots) {
2612
+ if (pathsOverlapFilesystemTree(resolvedCandidate, root))
2613
+ return true;
2614
+ }
2615
+ return false;
2616
+ }
2617
+ async function scanCustomPlanDirs(coveredPaths, into) {
2618
+ const dirs = getCustomPlanDirs();
2619
+ const userPlansDir = resolve3(getUserPlansDir());
2620
+ for (const dir of dirs) {
2621
+ const resolved = resolve3(dir);
2622
+ if (overlapsAnyRoot(resolved, coveredPaths)) {
2623
+ console.log(`[agendex] skipping custom dir (overlaps adapter / discovered coverage): ${dir}`);
2624
+ continue;
2625
+ }
2626
+ if (pathsOverlapFilesystemTree(resolved, userPlansDir)) {
2627
+ console.log(`[agendex] skipping custom dir (overlaps user plans): ${dir}`);
2628
+ continue;
2629
+ }
2630
+ if (!existsSync4(dir)) {
2631
+ console.log(`[agendex] skipping custom dir (not found): ${dir}`);
2632
+ continue;
2633
+ }
2634
+ const files = await walkDir(dir);
2635
+ const dirBasename = dir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "custom";
2636
+ let count = 0;
2637
+ for (const file of files) {
2638
+ if (!file.endsWith(".md"))
2639
+ continue;
2640
+ const plan = await parseGenericMarkdownPlan(file, {
2641
+ source: "custom-dir",
2642
+ customDir: dir,
2643
+ agentHint: dirBasename
2644
+ });
2645
+ if (plan) {
2646
+ into.set(plan.id, plan);
2647
+ count++;
2532
2648
  }
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 {}
2649
+ }
2650
+ console.log(`[agendex] custom dir: ${dir} (${count} plans)`);
2549
2651
  }
2550
2652
  }
2551
2653
  async function scan() {
2552
2654
  const adapters = getActiveAdapters();
2553
- store.clear();
2655
+ const next = new Map;
2554
2656
  const coveredPaths = new Set;
2555
2657
  for (const adapter of adapters) {
2556
2658
  for (const searchPath of adapter.getSearchPaths()) {
2557
- coveredPaths.add(resolve2(searchPath));
2659
+ coveredPaths.add(resolve3(searchPath));
2558
2660
  const files = await walkDir(searchPath);
2559
2661
  for (const file of files) {
2560
2662
  if (!adapter.matches(file))
2561
2663
  continue;
2562
2664
  const plans = await adapter.parse(file);
2563
2665
  for (const plan of plans) {
2564
- store.set(plan.id, plan);
2666
+ next.set(plan.id, plan);
2565
2667
  }
2566
2668
  }
2567
2669
  }
2568
2670
  }
2569
2671
  const discovered = discoverProjectPlanDirs();
2570
2672
  for (const { dir, agent } of discovered) {
2571
- if (coveredPaths.has(resolve2(dir)))
2673
+ const resolvedDir = resolve3(dir);
2674
+ if (coveredPaths.has(resolvedDir))
2572
2675
  continue;
2573
2676
  const adapter = adapters.find((a) => a.agent === agent);
2574
2677
  if (!adapter)
@@ -2579,12 +2682,15 @@ async function scan() {
2579
2682
  continue;
2580
2683
  const plans = await adapter.parse(file);
2581
2684
  for (const plan of plans) {
2582
- store.set(plan.id, plan);
2685
+ next.set(plan.id, plan);
2583
2686
  }
2584
2687
  }
2688
+ coveredPaths.add(resolvedDir);
2585
2689
  console.log(`[agendex] discovered project plans: ${dir}`);
2586
2690
  }
2587
- await scanUserPlans();
2691
+ await scanUserPlans(next);
2692
+ await scanCustomPlanDirs(coveredPaths, next);
2693
+ store = next;
2588
2694
  notifyPlansChanged();
2589
2695
  console.log(`[agendex] indexed ${store.size} plans from ${adapters.length} adapters`);
2590
2696
  }
@@ -2593,13 +2699,13 @@ function getAll() {
2593
2699
  }
2594
2700
  async function rescanFile(filePath) {
2595
2701
  const adapters = getActiveAdapters();
2596
- const normalized = resolve2(filePath);
2702
+ const normalized = resolve3(filePath);
2597
2703
  for (const adapter of adapters) {
2598
2704
  if (!adapter.matches(filePath))
2599
2705
  continue;
2600
- const discoveredDirs = discoverProjectPlanDirs().filter((d2) => d2.agent === adapter.agent).map((d2) => resolve2(d2.dir));
2706
+ const discoveredDirs = discoverProjectPlanDirs().filter((d2) => d2.agent === adapter.agent).map((d2) => resolve3(d2.dir));
2601
2707
  const allSearchPaths = [
2602
- ...adapter.getSearchPaths().map((sp) => resolve2(sp)),
2708
+ ...adapter.getSearchPaths().map((sp) => resolve3(sp)),
2603
2709
  ...discoveredDirs
2604
2710
  ];
2605
2711
  const isInSearchPath = allSearchPaths.some((sp) => normalized.startsWith(sp + sep2) || normalized === sp);
@@ -2612,18 +2718,58 @@ async function rescanFile(filePath) {
2612
2718
  notifyPlansChanged();
2613
2719
  return plans;
2614
2720
  }
2721
+ const userPlansDir = resolve3(getUserPlansDir());
2722
+ if (normalized.endsWith(".md") && (normalized.startsWith(userPlansDir + sep2) || normalized === userPlansDir)) {
2723
+ const plan = await parseGenericMarkdownPlan(filePath, { userCreated: true });
2724
+ if (plan) {
2725
+ store.set(plan.id, plan);
2726
+ notifyPlansChanged();
2727
+ return [plan];
2728
+ }
2729
+ }
2730
+ if (normalized.endsWith(".md")) {
2731
+ const customDirs = getCustomPlanDirs();
2732
+ for (const dir of customDirs) {
2733
+ const resolvedDir = resolve3(dir);
2734
+ if (normalized.startsWith(resolvedDir + sep2) || normalized === resolvedDir) {
2735
+ const plan = await parseGenericMarkdownPlan(filePath, {
2736
+ source: "custom-dir",
2737
+ customDir: dir
2738
+ });
2739
+ if (plan) {
2740
+ store.set(plan.id, plan);
2741
+ notifyPlansChanged();
2742
+ return [plan];
2743
+ }
2744
+ }
2745
+ }
2746
+ }
2615
2747
  return [];
2616
2748
  }
2617
2749
  // ../shared/src/services/watcher.ts
2618
2750
  import { existsSync as existsSync5, watch } from "node:fs";
2619
- import { join as join9, resolve as resolve3 } from "node:path";
2751
+ import { join as join9, resolve as resolve4 } from "node:path";
2620
2752
  var debounceTimer = null;
2621
2753
  var pendingFiles = new Set;
2754
+ var activeWatchers = [];
2755
+ function closeAllWatchers() {
2756
+ if (debounceTimer) {
2757
+ clearTimeout(debounceTimer);
2758
+ debounceTimer = null;
2759
+ }
2760
+ pendingFiles.clear();
2761
+ for (const watcher of activeWatchers) {
2762
+ try {
2763
+ watcher.close();
2764
+ } catch {}
2765
+ }
2766
+ activeWatchers.length = 0;
2767
+ }
2622
2768
  function watchDir(dir, matchFn, onChange) {
2623
2769
  if (!existsSync5(dir))
2624
2770
  return;
2625
2771
  try {
2626
- watch(dir, { recursive: true }, async (_event, filename) => {
2772
+ const watcher = watch(dir, { recursive: true }, async (_event, filename) => {
2627
2773
  if (!filename)
2628
2774
  return;
2629
2775
  const fullPath = join9(dir, filename);
@@ -2643,27 +2789,51 @@ function watchDir(dir, matchFn, onChange) {
2643
2789
  onChange?.(allPlans);
2644
2790
  }, 300);
2645
2791
  });
2792
+ activeWatchers.push(watcher);
2646
2793
  console.log(`[agendex] watching ${dir}`);
2647
2794
  } catch {}
2648
2795
  }
2649
2796
  function startWatching(onChange) {
2797
+ closeAllWatchers();
2798
+ setupWatchers(onChange);
2799
+ }
2800
+ function stopWatching() {
2801
+ closeAllWatchers();
2802
+ }
2803
+ function setupWatchers(onChange) {
2650
2804
  const adapters = getActiveAdapters();
2651
2805
  const watchedPaths = new Set;
2652
2806
  for (const adapter of adapters) {
2653
2807
  for (const watchPath of adapter.getWatchPaths()) {
2654
- watchedPaths.add(resolve3(watchPath));
2808
+ watchedPaths.add(resolve4(watchPath));
2655
2809
  watchDir(watchPath, (f) => adapter.matches(f), onChange);
2656
2810
  }
2657
2811
  }
2658
2812
  const discovered = discoverProjectPlanDirs();
2659
2813
  for (const { dir, agent } of discovered) {
2660
- if (watchedPaths.has(resolve3(dir)))
2814
+ if (watchedPaths.has(resolve4(dir)))
2661
2815
  continue;
2662
2816
  const adapter = adapters.find((a) => a.agent === agent);
2663
2817
  if (!adapter)
2664
2818
  continue;
2819
+ watchedPaths.add(resolve4(dir));
2665
2820
  watchDir(dir, (f) => adapter.matches(f), onChange);
2666
2821
  }
2822
+ const customDirs = getCustomPlanDirs();
2823
+ for (const dir of customDirs) {
2824
+ const resolvedCustom = resolve4(dir);
2825
+ let overlaps = false;
2826
+ for (const watched of watchedPaths) {
2827
+ if (pathsOverlapFilesystemTree(resolvedCustom, watched)) {
2828
+ overlaps = true;
2829
+ break;
2830
+ }
2831
+ }
2832
+ if (overlaps)
2833
+ continue;
2834
+ watchedPaths.add(resolvedCustom);
2835
+ watchDir(dir, (f) => f.endsWith(".md"), onChange);
2836
+ }
2667
2837
  }
2668
2838
  // src/api.ts
2669
2839
  import { request as httpRequest } from "node:http";
@@ -2853,7 +3023,7 @@ function requestText(urlString, options) {
2853
3023
  if (options.body) {
2854
3024
  headers["Content-Length"] = String(Buffer.byteLength(options.body));
2855
3025
  }
2856
- return new Promise((resolve4, reject) => {
3026
+ return new Promise((resolve5, reject) => {
2857
3027
  const req = request(url, {
2858
3028
  agent: false,
2859
3029
  headers,
@@ -2865,7 +3035,7 @@ function requestText(urlString, options) {
2865
3035
  body += chunk;
2866
3036
  });
2867
3037
  res.on("end", () => {
2868
- resolve4({
3038
+ resolve5({
2869
3039
  status: res.statusCode ?? 0,
2870
3040
  body
2871
3041
  });
@@ -2979,7 +3149,8 @@ async function login(siteUrlOverride) {
2979
3149
  token: existing?.token,
2980
3150
  cloudToken: callback.token,
2981
3151
  convexUrl: callback.convexUrl,
2982
- enabledAdapters: existing?.enabledAdapters ?? []
3152
+ enabledAdapters: existing?.enabledAdapters ?? [],
3153
+ customPlanDirs: existing?.customPlanDirs ?? []
2983
3154
  };
2984
3155
  saveConfig(config);
2985
3156
  console.log(`[agendex] Logged in successfully!`);
@@ -2996,7 +3167,8 @@ function logout() {
2996
3167
  token: existing.token,
2997
3168
  cloudToken: undefined,
2998
3169
  convexUrl: undefined,
2999
- enabledAdapters: existing.enabledAdapters
3170
+ enabledAdapters: existing.enabledAdapters,
3171
+ customPlanDirs: existing.customPlanDirs
3000
3172
  };
3001
3173
  saveConfig(config);
3002
3174
  console.log("[agendex] Logged out. Cloud token removed.");
@@ -3007,14 +3179,14 @@ async function startCallbackServer() {
3007
3179
  let timeout;
3008
3180
  let settle;
3009
3181
  let fail;
3010
- const result = new Promise((resolve4, reject) => {
3011
- settle = resolve4;
3182
+ const result = new Promise((resolve5, reject) => {
3183
+ settle = resolve5;
3012
3184
  fail = reject;
3013
3185
  });
3014
3186
  const finish = (value) => {
3015
3187
  if (!settle || !fail)
3016
3188
  return;
3017
- const resolve4 = settle;
3189
+ const resolve5 = settle;
3018
3190
  const reject = fail;
3019
3191
  settle = undefined;
3020
3192
  fail = undefined;
@@ -3031,7 +3203,7 @@ async function startCallbackServer() {
3031
3203
  reject(value);
3032
3204
  return;
3033
3205
  }
3034
- resolve4(value);
3206
+ resolve5(value);
3035
3207
  };
3036
3208
  server.on("request", (req, res) => {
3037
3209
  const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -3065,8 +3237,8 @@ async function startCallbackServer() {
3065
3237
  sockets.delete(socket);
3066
3238
  });
3067
3239
  });
3068
- await new Promise((resolve4, reject) => {
3069
- server.listen(0, "127.0.0.1", () => resolve4());
3240
+ await new Promise((resolve5, reject) => {
3241
+ server.listen(0, "127.0.0.1", () => resolve5());
3070
3242
  server.once("error", reject);
3071
3243
  });
3072
3244
  server.once("error", (error) => {
@@ -3146,7 +3318,7 @@ function spawnBrowser(command, args, options = {}) {
3146
3318
 
3147
3319
  // src/daemon.ts
3148
3320
  import { spawn as spawn2 } from "node:child_process";
3149
- import { resolve as resolve4 } from "node:path";
3321
+ import { resolve as resolve5 } from "node:path";
3150
3322
  import { fileURLToPath } from "node:url";
3151
3323
 
3152
3324
  // src/sync-cache.ts
@@ -3309,6 +3481,7 @@ async function runWorker() {
3309
3481
  });
3310
3482
  console.log(`[agendex] daemon running. Watching for file changes...`);
3311
3483
  async function gracefulShutdown() {
3484
+ stopWatching();
3312
3485
  await sendShutdown();
3313
3486
  process.exit(0);
3314
3487
  }
@@ -3332,17 +3505,17 @@ async function startSupervisor() {
3332
3505
  };
3333
3506
  process.on("SIGTERM", shutdown);
3334
3507
  process.on("SIGINT", shutdown);
3335
- const scriptPath = resolve4(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
3508
+ const scriptPath = resolve5(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
3336
3509
  const restartTimes = [];
3337
3510
  while (!stopping) {
3338
3511
  workerProc = spawn2(process.execPath, [scriptPath, "start", "--worker"], {
3339
3512
  stdio: ["ignore", "inherit", "inherit"]
3340
3513
  });
3341
- const exitCode = await new Promise((resolve5) => {
3342
- workerProc?.once("exit", (code) => resolve5(code));
3514
+ const exitCode = await new Promise((resolve6) => {
3515
+ workerProc?.once("exit", (code) => resolve6(code));
3343
3516
  workerProc?.once("error", (error) => {
3344
3517
  console.error("[agendex] failed to spawn worker:", error);
3345
- resolve5(1);
3518
+ resolve6(1);
3346
3519
  });
3347
3520
  });
3348
3521
  workerProc = null;
@@ -3421,7 +3594,7 @@ import { join as join12 } from "node:path";
3421
3594
  // package.json
3422
3595
  var package_default = {
3423
3596
  name: "agendex-cli",
3424
- version: "0.11.0",
3597
+ version: "0.13.0",
3425
3598
  description: "Agendex CLI for login, sync, and daemon workflows",
3426
3599
  homepage: "https://github.com/Tyru5/Agendex#readme",
3427
3600
  repository: {
@@ -3568,7 +3741,7 @@ function firstCommandToken(argv) {
3568
3741
  return;
3569
3742
  }
3570
3743
  var command = firstCommandToken(args) ?? "start";
3571
- var cliEntry = resolve5(process.argv[1] ?? fileURLToPath2(import.meta.url));
3744
+ var cliEntry = resolve6(process.argv[1] ?? fileURLToPath2(import.meta.url));
3572
3745
  async function main() {
3573
3746
  const isInternal = args.includes("--daemon") || args.includes("--worker");
3574
3747
  if (command === "--version" || command === "-v") {
@@ -3583,6 +3756,9 @@ async function main() {
3583
3756
  "open",
3584
3757
  "view",
3585
3758
  "cleanup",
3759
+ "add-dir",
3760
+ "remove-dir",
3761
+ "list-dirs",
3586
3762
  "help",
3587
3763
  "--help",
3588
3764
  "-h"
@@ -3758,6 +3934,112 @@ async function main() {
3758
3934
  }
3759
3935
  return 0;
3760
3936
  }
3937
+ case "add-dir": {
3938
+ const dirPath = args.find((a) => a !== "add-dir" && a !== "--dev" && !a.startsWith("--"));
3939
+ if (!dirPath || !dirPath.trim()) {
3940
+ writeStderr("[agendex] usage: agendex add-dir <path>");
3941
+ return 1;
3942
+ }
3943
+ const resolved = resolveCustomPlanDirPath(dirPath);
3944
+ if (!existsSync9(resolved)) {
3945
+ writeStderr(`[agendex] path does not exist: ${resolved}`);
3946
+ return 1;
3947
+ }
3948
+ if (!statSync2(resolved).isDirectory()) {
3949
+ writeStderr(`[agendex] path is not a directory: ${resolved}`);
3950
+ return 1;
3951
+ }
3952
+ if (args.includes("--live")) {
3953
+ const cfg = loadConfig();
3954
+ const token = cfg?.token;
3955
+ if (!token) {
3956
+ writeStderr("[agendex] no local token found in config — is the server running?");
3957
+ return 1;
3958
+ }
3959
+ const port = process.env.PORT ?? "4890";
3960
+ const { request } = await import("node:http");
3961
+ const body = JSON.stringify({ path: resolved });
3962
+ try {
3963
+ const res = await new Promise((resolve7, reject) => {
3964
+ const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
3965
+ method: "POST",
3966
+ headers: {
3967
+ Authorization: `Bearer ${token}`,
3968
+ "Content-Type": "application/json",
3969
+ "Content-Length": String(Buffer.byteLength(body))
3970
+ }
3971
+ }, (res2) => {
3972
+ let data = "";
3973
+ res2.setEncoding("utf8");
3974
+ res2.on("data", (chunk) => {
3975
+ data += chunk;
3976
+ });
3977
+ res2.on("end", () => resolve7({ status: res2.statusCode ?? 0, body: data }));
3978
+ res2.on("error", reject);
3979
+ });
3980
+ req.on("error", reject);
3981
+ req.write(body);
3982
+ req.end();
3983
+ });
3984
+ if (res.status >= 200 && res.status < 300) {
3985
+ writeStdout(`[agendex] added custom plan dir: ${resolved}`);
3986
+ writeStdout(`[agendex] server notified — scanning + watching now`);
3987
+ } else {
3988
+ writeStderr(`[agendex] server returned ${res.status}: ${res.body}`);
3989
+ return 1;
3990
+ }
3991
+ } catch (err) {
3992
+ const msg = err instanceof Error ? err.message : String(err);
3993
+ writeStderr(`[agendex] could not reach local server on port ${port}: ${msg}`);
3994
+ return 1;
3995
+ }
3996
+ } else {
3997
+ const cfg = loadConfig();
3998
+ const currentDirs = cfg?.customPlanDirs ?? [];
3999
+ const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
4000
+ saveConfig({
4001
+ ...cfg ?? { configVersion: 3, enabledAdapters: [] },
4002
+ customPlanDirs: updated
4003
+ });
4004
+ writeStdout(`[agendex] added custom plan dir: ${resolved}`);
4005
+ writeStdout(`[agendex] daemon will pick up the change automatically`);
4006
+ }
4007
+ return 0;
4008
+ }
4009
+ case "remove-dir": {
4010
+ const dirPath = args.find((a) => a !== "remove-dir" && a !== "--dev" && !a.startsWith("--"));
4011
+ if (!dirPath || !dirPath.trim()) {
4012
+ writeStderr("[agendex] usage: agendex remove-dir <path>");
4013
+ return 1;
4014
+ }
4015
+ const resolved = resolveCustomPlanDirPath(dirPath);
4016
+ const cfg = loadConfig();
4017
+ const currentDirs = cfg?.customPlanDirs ?? [];
4018
+ const updated = currentDirs.filter((d2) => d2 !== resolved);
4019
+ if (updated.length === currentDirs.length) {
4020
+ writeStderr(`[agendex] directory not in custom plan dirs: ${resolved}`);
4021
+ return 1;
4022
+ }
4023
+ saveConfig({
4024
+ ...cfg ?? { configVersion: 3, enabledAdapters: [] },
4025
+ customPlanDirs: updated
4026
+ });
4027
+ writeStdout(`[agendex] removed custom plan dir: ${resolved}`);
4028
+ return 0;
4029
+ }
4030
+ case "list-dirs": {
4031
+ const cfg = loadConfig();
4032
+ const dirs = cfg?.customPlanDirs ?? [];
4033
+ if (dirs.length === 0) {
4034
+ writeStdout("[agendex] no custom plan directories configured");
4035
+ } else {
4036
+ writeStdout(`[agendex] custom plan directories (${dirs.length}):`);
4037
+ for (const dir of dirs) {
4038
+ writeStdout(` - ${dir}`);
4039
+ }
4040
+ }
4041
+ return 0;
4042
+ }
3761
4043
  case "status": {
3762
4044
  const config = loadConfig();
3763
4045
  const pidInfo = readPidInfo();
@@ -3768,6 +4050,11 @@ async function main() {
3768
4050
  writeStdout(`[agendex] Cloud token: ${config?.cloudToken ? "set" : "not set"}`);
3769
4051
  writeStdout(`[agendex] Convex URL: ${config?.convexUrl ?? "not set"}`);
3770
4052
  writeStdout(`[agendex] Enabled adapters: ${config?.enabledAdapters.join(", ") || "none"}`);
4053
+ const customDirs = config?.customPlanDirs ?? [];
4054
+ writeStdout(`[agendex] Custom plan dirs: ${customDirs.length > 0 ? customDirs.length : "none"}`);
4055
+ for (const dir of customDirs) {
4056
+ writeStdout(` - ${dir}`);
4057
+ }
3771
4058
  writeStdout(`[agendex] Daemon: ${running ? `running (PID ${pid})` : "not running"}`);
3772
4059
  if (running && pidInfo?.startedAtMs) {
3773
4060
  writeStdout(`[agendex] Uptime: ${formatDuration(Date.now() - pidInfo.startedAtMs)}`);
@@ -3832,6 +4119,10 @@ Usage:
3832
4119
  agendex view <url> Open a shared plan URL in your browser
3833
4120
  agendex logout Clear stored cloud token
3834
4121
  agendex configure Select which agents/adapters to index
4122
+ agendex add-dir <path> Add a custom directory to scan for plans
4123
+ agendex add-dir <path> --live Add dir and notify running server immediately
4124
+ agendex remove-dir <path> Remove a custom directory
4125
+ agendex list-dirs List custom plan directories
3835
4126
  agendex sync One-shot scan + sync to cloud (skips unchanged plans)
3836
4127
  agendex sync --force Re-sync all plans, ignoring cache
3837
4128
  agendex cleanup Interactively remove cloud daemons
@@ -3865,13 +4156,13 @@ function flushStream(stream) {
3865
4156
  if (stream.destroyed || !stream.writable) {
3866
4157
  return Promise.resolve();
3867
4158
  }
3868
- return new Promise((resolve6, reject) => {
4159
+ return new Promise((resolve7, reject) => {
3869
4160
  stream.write("", (error) => {
3870
4161
  if (error) {
3871
4162
  reject(error);
3872
4163
  return;
3873
4164
  }
3874
- resolve6();
4165
+ resolve7();
3875
4166
  });
3876
4167
  });
3877
4168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {