agendex-cli 2.0.0 → 2.0.1

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 +603 -264
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4473,6 +4473,158 @@ function spawnBrowser(command, args, options = {}) {
4473
4473
  child.unref();
4474
4474
  }
4475
4475
 
4476
+ // src/help.ts
4477
+ var COMMAND_WIDTH = 38;
4478
+ var FLAG_WIDTH = 18;
4479
+ var ANSI = {
4480
+ reset: "\x1B[0m",
4481
+ bold: "\x1B[1m",
4482
+ gray: "\x1B[90m",
4483
+ cyan: "\x1B[36m",
4484
+ green: "\x1B[32m",
4485
+ yellow: "\x1B[33m"
4486
+ };
4487
+ var HELP_GROUPS = [
4488
+ {
4489
+ title: "Quick start",
4490
+ commands: [
4491
+ {
4492
+ command: "start",
4493
+ description: "Start the background sync daemon",
4494
+ examples: ["agendex", "agendex start"]
4495
+ },
4496
+ { command: "status", description: "Show local/cloud health and recommended next steps" },
4497
+ {
4498
+ command: "open",
4499
+ description: "Open the Agendex dashboard in your browser",
4500
+ examples: ["agendex open --url <self-hosted-url>"]
4501
+ }
4502
+ ]
4503
+ },
4504
+ {
4505
+ title: "Cloud account",
4506
+ commands: [
4507
+ {
4508
+ command: "login",
4509
+ description: "Authenticate via browser OAuth",
4510
+ examples: ["agendex login --url <self-hosted-url>"]
4511
+ },
4512
+ { command: "logout", description: "Clear the stored cloud token" },
4513
+ { command: "view <url>", description: "Open a shared plan URL in your browser" }
4514
+ ]
4515
+ },
4516
+ {
4517
+ title: "Plan sources",
4518
+ commands: [
4519
+ { command: "configure", description: "Select which agents/adapters to index" },
4520
+ {
4521
+ command: "add-dir <path>",
4522
+ description: "Add a custom directory to scan for plans",
4523
+ examples: ["agendex add-dir ~/plans --live"]
4524
+ },
4525
+ { command: "remove-dir <path>", description: "Remove a custom plan directory" },
4526
+ { command: "list-dirs", description: "List configured custom plan directories" }
4527
+ ]
4528
+ },
4529
+ {
4530
+ title: "Sync & upload",
4531
+ commands: [
4532
+ {
4533
+ command: "sync",
4534
+ description: "Run a one-shot scan and cloud sync",
4535
+ examples: ["agendex sync --force"]
4536
+ },
4537
+ {
4538
+ command: "upload <path>",
4539
+ description: "Upload a single Markdown plan file",
4540
+ examples: ["agendex upload plan.md --agent claude-code --open"]
4541
+ }
4542
+ ]
4543
+ },
4544
+ {
4545
+ title: "Hooks & review",
4546
+ commands: [
4547
+ { command: "hooks status", description: "Show Claude Code, Codex, and Pi hook status" },
4548
+ {
4549
+ command: "hooks install <agent|all>",
4550
+ description: "Install hook integration",
4551
+ examples: ["agendex hooks install claude-code --preview"]
4552
+ },
4553
+ { command: "hooks uninstall <agent|all>", description: "Remove managed hook entries" },
4554
+ { command: "review-plan --hook --agent <agent>", description: "Run hook-native plan review" }
4555
+ ]
4556
+ },
4557
+ {
4558
+ title: "Maintenance",
4559
+ commands: [
4560
+ { command: "cleanup", description: "Interactively remove cloud daemon records" },
4561
+ { command: "cleanup --stale", description: "Auto-remove stale cloud daemon records" },
4562
+ { command: "upgrade", description: "Upgrade the globally installed CLI" },
4563
+ { command: "upgrade --force", description: "Reinstall latest even when up to date" },
4564
+ { command: "help", description: "Show this help message" },
4565
+ { command: "--version, -v", description: "Print CLI version" }
4566
+ ]
4567
+ }
4568
+ ];
4569
+ var FLAGS = [
4570
+ { flag: "--dev", description: "Use the dev environment (~/.agendex-dev config dir)" }
4571
+ ];
4572
+ function supportsColor() {
4573
+ if (process.env.NO_COLOR)
4574
+ return false;
4575
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
4576
+ return true;
4577
+ return Boolean(process.stdout.isTTY);
4578
+ }
4579
+ function paint(enabled, code, text) {
4580
+ return enabled ? `${code}${text}${ANSI.reset}` : text;
4581
+ }
4582
+ function createStyles(color) {
4583
+ return {
4584
+ title: (text) => paint(color, `${ANSI.bold}${ANSI.cyan}`, text),
4585
+ section: (text) => paint(color, ANSI.yellow, text),
4586
+ command: (text) => paint(color, ANSI.green, text),
4587
+ muted: (text) => paint(color, ANSI.gray, text)
4588
+ };
4589
+ }
4590
+ function commandLine(styles, entry) {
4591
+ return ` ${styles.command(entry.command.padEnd(COMMAND_WIDTH))}${entry.description}`;
4592
+ }
4593
+ function exampleLine(styles, examples) {
4594
+ return ` ${styles.muted(examples.join(" | "))}`;
4595
+ }
4596
+ function flagLine(styles, flag) {
4597
+ return ` ${styles.command(flag.flag.padEnd(FLAG_WIDTH))}${flag.description}`;
4598
+ }
4599
+ function renderHelp(options) {
4600
+ const styles = createStyles(options.color ?? supportsColor());
4601
+ const lines = [];
4602
+ lines.push(styles.title("Agendex: local coding-agent plan sync"));
4603
+ lines.push(styles.muted(`CLI v${options.cliVersion}`));
4604
+ lines.push("");
4605
+ lines.push(`Usage: ${styles.command("agendex")} ${styles.muted("[OPTIONS] [COMMAND]")}`);
4606
+ lines.push("");
4607
+ lines.push(styles.section("Commands:"));
4608
+ for (const group of HELP_GROUPS) {
4609
+ lines.push(` ${styles.section(`${group.title}:`)}`);
4610
+ for (const command of group.commands) {
4611
+ lines.push(commandLine(styles, command));
4612
+ if (command.examples && command.examples.length > 0) {
4613
+ lines.push(exampleLine(styles, command.examples));
4614
+ }
4615
+ }
4616
+ lines.push("");
4617
+ }
4618
+ lines.push(styles.section("Flags:"));
4619
+ for (const flag of FLAGS)
4620
+ lines.push(flagLine(styles, flag));
4621
+ lines.push("");
4622
+ lines.push(styles.section("Tip:"));
4623
+ lines.push(` ${styles.muted("Run")} ${styles.command("agendex status")} ${styles.muted("to see daemon health, cloud sync state, and next steps.")}`);
4624
+ return lines.join(`
4625
+ `);
4626
+ }
4627
+
4476
4628
  // src/daemon.ts
4477
4629
  import { spawn as spawn2 } from "node:child_process";
4478
4630
  import { hostname as osHostname2 } from "node:os";
@@ -5676,6 +5828,254 @@ async function runHookReviewCommand(args) {
5676
5828
  return 1;
5677
5829
  }
5678
5830
 
5831
+ // src/status.ts
5832
+ var LABEL_WIDTH = 18;
5833
+ var ACTION_WIDTH = 26;
5834
+ var ANSI2 = {
5835
+ reset: "\x1B[0m",
5836
+ bold: "\x1B[1m",
5837
+ dim: "\x1B[2m",
5838
+ gray: "\x1B[90m",
5839
+ cyan: "\x1B[36m",
5840
+ green: "\x1B[32m",
5841
+ yellow: "\x1B[33m",
5842
+ red: "\x1B[31m",
5843
+ blue: "\x1B[34m"
5844
+ };
5845
+ function supportsColor2() {
5846
+ if (process.env.NO_COLOR)
5847
+ return false;
5848
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
5849
+ return true;
5850
+ return Boolean(process.stdout.isTTY);
5851
+ }
5852
+ function paint2(enabled, code, text) {
5853
+ return enabled ? `${code}${text}${ANSI2.reset}` : text;
5854
+ }
5855
+ function createStyles2(color) {
5856
+ return {
5857
+ title: (text) => paint2(color, `${ANSI2.bold}${ANSI2.cyan}`, text),
5858
+ section: (text) => paint2(color, ANSI2.yellow, text),
5859
+ key: (text) => paint2(color, ANSI2.green, text),
5860
+ value: (text) => text,
5861
+ muted: (text) => paint2(color, ANSI2.gray, text),
5862
+ status(kind, text) {
5863
+ if (kind === "success")
5864
+ return paint2(color, ANSI2.green, text);
5865
+ if (kind === "warning")
5866
+ return paint2(color, ANSI2.yellow, text);
5867
+ if (kind === "danger")
5868
+ return paint2(color, ANSI2.red, text);
5869
+ if (kind === "info")
5870
+ return paint2(color, ANSI2.blue, text);
5871
+ return paint2(color, ANSI2.gray, text);
5872
+ }
5873
+ };
5874
+ }
5875
+ function badge(styles, kind, label) {
5876
+ let icon = "•";
5877
+ if (kind === "success")
5878
+ icon = "✓";
5879
+ else if (kind === "warning")
5880
+ icon = "!";
5881
+ else if (kind === "danger")
5882
+ icon = "×";
5883
+ return styles.status(kind, `${icon} ${label}`);
5884
+ }
5885
+ function isPresent(value) {
5886
+ return value !== null && value !== undefined;
5887
+ }
5888
+ function row(styles, label, status, detail) {
5889
+ const labelCell = styles.key(label.padEnd(LABEL_WIDTH));
5890
+ const suffix = detail ? ` ${styles.muted(detail)}` : "";
5891
+ return ` ${labelCell}${status}${suffix}`;
5892
+ }
5893
+ function actionRow(styles, command, description) {
5894
+ return ` ${styles.key(command.padEnd(ACTION_WIDTH))}${description}`;
5895
+ }
5896
+ function listItem(styles, value) {
5897
+ return ` ${styles.muted("•")} ${value}`;
5898
+ }
5899
+ function summarizeList(items, maxItems = 4) {
5900
+ if (items.length === 0)
5901
+ return "";
5902
+ if (items.length <= maxItems)
5903
+ return items.join(", ");
5904
+ const visible = items.slice(0, maxItems).join(", ");
5905
+ return `${visible} +${items.length - maxItems} more`;
5906
+ }
5907
+ function formatDuration(ms) {
5908
+ const safeMs = Number.isFinite(ms) ? Math.max(0, Math.floor(ms)) : 0;
5909
+ const seconds = Math.floor(safeMs / 1000);
5910
+ if (seconds < 60)
5911
+ return `${seconds}s`;
5912
+ const minutes = Math.floor(seconds / 60);
5913
+ const remainingSeconds = seconds % 60;
5914
+ if (minutes < 60)
5915
+ return `${minutes}m ${remainingSeconds}s`;
5916
+ const hours = Math.floor(minutes / 60);
5917
+ const remainingMinutes = minutes % 60;
5918
+ if (hours < 24)
5919
+ return `${hours}h ${remainingMinutes}m`;
5920
+ const days = Math.floor(hours / 24);
5921
+ const remainingHours = hours % 24;
5922
+ return `${days}d ${remainingHours}h`;
5923
+ }
5924
+ function localDaemonDetail(options, now) {
5925
+ if (!options.running)
5926
+ return "run `agendex start` to begin background sync";
5927
+ const parts = [];
5928
+ if (isPresent(options.pidInfo?.pid))
5929
+ parts.push(`PID ${options.pidInfo.pid}`);
5930
+ if (isPresent(options.pidInfo?.startedAtMs)) {
5931
+ parts.push(`up ${formatDuration(now - options.pidInfo.startedAtMs)}`);
5932
+ } else {
5933
+ parts.push("uptime unknown");
5934
+ }
5935
+ if (options.pidInfo?.hostname)
5936
+ parts.push(`host ${options.pidInfo.hostname}`);
5937
+ else
5938
+ parts.push("host unknown");
5939
+ return parts.join(" • ");
5940
+ }
5941
+ function isDeviceAlive(device, now) {
5942
+ if (!isPresent(device.lastSeenAt))
5943
+ return false;
5944
+ return now - device.lastSeenAt < CLI_DAEMON_STALE_AFTER_MS;
5945
+ }
5946
+ function sortDevices(devices, localDeviceId, now) {
5947
+ return [...devices].sort((a, b) => {
5948
+ const localDiff = Number(b.deviceId === localDeviceId) - Number(a.deviceId === localDeviceId);
5949
+ if (localDiff !== 0)
5950
+ return localDiff;
5951
+ const aliveDiff = Number(isDeviceAlive(b, now)) - Number(isDeviceAlive(a, now));
5952
+ if (aliveDiff !== 0)
5953
+ return aliveDiff;
5954
+ return (a.hostname ?? "").localeCompare(b.hostname ?? "");
5955
+ });
5956
+ }
5957
+ function deviceLines({ styles, device, localDeviceId, now }) {
5958
+ const alive = isDeviceAlive(device, now);
5959
+ const statusText = alive ? "✓ alive" : "! stale";
5960
+ const statusCell = styles.status(alive ? "success" : "warning", statusText.padEnd(12));
5961
+ const hostname2 = device.hostname ?? "unknown host";
5962
+ const isLocalDevice = localDeviceId !== undefined && device.deviceId === localDeviceId;
5963
+ const localMarker = isLocalDevice ? " (this machine)" : "";
5964
+ const pid = isPresent(device.pid) ? `PID ${device.pid}` : "PID unknown";
5965
+ const uptime = isPresent(device.startedAtMs) ? `up ${formatDuration(now - device.startedAtMs)}` : "uptime unknown";
5966
+ const seen = isPresent(device.lastSeenAt) ? `seen ${formatDuration(now - device.lastSeenAt)} ago` : "last seen unknown";
5967
+ const ip = device.ipAddress ?? "IP unknown";
5968
+ return [
5969
+ ` ${statusCell}${styles.value(hostname2)}${styles.muted(localMarker)}`,
5970
+ ` ${styles.muted([pid, uptime, seen, ip].join(" • "))}`
5971
+ ];
5972
+ }
5973
+ function addCloudDaemonLines({ lines, styles, options, cloudReady, now }) {
5974
+ if (!cloudReady) {
5975
+ lines.push(row(styles, "Daemon registry", badge(styles, "warning", "offline"), "login required"));
5976
+ return;
5977
+ }
5978
+ if (options.cloudDaemonError?.kind === "auth-expired") {
5979
+ lines.push(row(styles, "Daemon registry", badge(styles, "danger", "token expired"), "run `agendex login`"));
5980
+ return;
5981
+ }
5982
+ if (options.cloudDaemonError?.kind === "unavailable") {
5983
+ lines.push(row(styles, "Daemon registry", badge(styles, "warning", "unavailable"), options.cloudDaemonError.message ?? "will retry on the next status check"));
5984
+ return;
5985
+ }
5986
+ const devices = options.devices ?? [];
5987
+ if (devices.length === 0) {
5988
+ lines.push(row(styles, "Daemon registry", badge(styles, "info", "empty"), "no cloud daemons reported"));
5989
+ return;
5990
+ }
5991
+ const aliveCount = devices.filter((device) => isDeviceAlive(device, now)).length;
5992
+ const staleCount = devices.length - aliveCount;
5993
+ lines.push(row(styles, "Daemon registry", badge(styles, "success", `${devices.length} device${devices.length === 1 ? "" : "s"}`), `${aliveCount} alive • ${staleCount} stale`));
5994
+ const localDeviceId = options.config?.deviceId;
5995
+ for (const device of sortDevices(devices, localDeviceId, now)) {
5996
+ lines.push(...deviceLines({ styles, device, localDeviceId, now }));
5997
+ }
5998
+ }
5999
+ function nextActions(options, now) {
6000
+ const config = options.config;
6001
+ const cloudReady = Boolean(config?.cloudToken && config.convexUrl);
6002
+ const adapters = config?.enabledAdapters ?? [];
6003
+ const devices = options.devices ?? [];
6004
+ const hasStaleDevices = devices.some((device) => !isDeviceAlive(device, now));
6005
+ const authExpired = options.cloudDaemonError?.kind === "auth-expired";
6006
+ const actions = [];
6007
+ if (!options.running) {
6008
+ actions.push({ command: "agendex start", description: "Start the background sync daemon" });
6009
+ }
6010
+ if (!cloudReady || authExpired) {
6011
+ actions.push({ command: "agendex login", description: "Connect or refresh cloud sync" });
6012
+ }
6013
+ if (adapters.length === 0) {
6014
+ actions.push({
6015
+ command: "agendex configure",
6016
+ description: "Choose which agent plan sources to index"
6017
+ });
6018
+ }
6019
+ if (hasStaleDevices) {
6020
+ actions.push({
6021
+ command: "agendex cleanup --stale",
6022
+ description: "Remove stale cloud daemon records"
6023
+ });
6024
+ }
6025
+ if (cloudReady) {
6026
+ actions.push({
6027
+ command: "agendex sync",
6028
+ description: "Run a one-shot scan and cloud sync now"
6029
+ });
6030
+ }
6031
+ actions.push({ command: "agendex open", description: "Open the Agendex dashboard" });
6032
+ const seen = new Set;
6033
+ return actions.filter((action) => {
6034
+ if (seen.has(action.command))
6035
+ return false;
6036
+ seen.add(action.command);
6037
+ return true;
6038
+ });
6039
+ }
6040
+ function renderStatus(options) {
6041
+ const styles = createStyles2(options.color ?? supportsColor2());
6042
+ const now = options.now ?? Date.now();
6043
+ const config = options.config;
6044
+ const adapters = config?.enabledAdapters ?? [];
6045
+ const customDirs = config?.customPlanDirs ?? [];
6046
+ const cloudReady = Boolean(config?.cloudToken && config.convexUrl);
6047
+ const lines = [];
6048
+ lines.push(styles.title("Agendex status"));
6049
+ lines.push(styles.muted(`Config: ${options.configPath}`));
6050
+ lines.push("");
6051
+ lines.push(styles.section("Local:"));
6052
+ lines.push(row(styles, "Daemon", options.running ? badge(styles, "success", "running") : badge(styles, "warning", "not running"), localDaemonDetail(options, now)));
6053
+ lines.push(row(styles, "Config file", config ? badge(styles, "success", "found") : badge(styles, "warning", "missing"), config ? `v${config.configVersion}` : "created on first start/configure"));
6054
+ lines.push(row(styles, "Local API token", config?.token ? badge(styles, "success", "set") : badge(styles, "warning", "missing"), config?.token ? "ready for OSS API auth" : "generated when the local app starts"));
6055
+ lines.push(row(styles, "CLI version", styles.value(`v${options.cliVersion}`)));
6056
+ lines.push("");
6057
+ lines.push(styles.section("Cloud:"));
6058
+ lines.push(row(styles, "Account", cloudReady ? badge(styles, "success", "logged in") : badge(styles, "warning", "not logged in"), config?.convexUrl ?? "run `agendex login` to enable cloud sync"));
6059
+ if (config?.siteUrl) {
6060
+ lines.push(row(styles, "Web app", styles.value(config.siteUrl)));
6061
+ }
6062
+ lines.push(row(styles, "Device ID", config?.deviceId ? badge(styles, "success", "registered") : badge(styles, "warning", "missing"), config?.deviceId ?? "created on the next daemon heartbeat"));
6063
+ addCloudDaemonLines({ lines, styles, options, cloudReady, now });
6064
+ lines.push("");
6065
+ lines.push(styles.section("Plan sources:"));
6066
+ lines.push(row(styles, "Adapters", adapters.length > 0 ? badge(styles, "success", `${adapters.length} enabled`) : badge(styles, "warning", "none enabled"), adapters.length > 0 ? summarizeList(adapters) : "run `agendex configure`"));
6067
+ lines.push(row(styles, "Custom dirs", customDirs.length > 0 ? badge(styles, "info", `${customDirs.length} configured`) : badge(styles, "info", "none"), customDirs.length > 0 ? "additional scan roots" : "using built-in agent directories"));
6068
+ for (const dir of customDirs)
6069
+ lines.push(listItem(styles, dir));
6070
+ lines.push("");
6071
+ lines.push(styles.section("Next steps:"));
6072
+ for (const action of nextActions(options, now)) {
6073
+ lines.push(actionRow(styles, action.command, action.description));
6074
+ }
6075
+ return lines.join(`
6076
+ `);
6077
+ }
6078
+
5679
6079
  // src/sync.ts
5680
6080
  import { hostname as osHostname3 } from "node:os";
5681
6081
  async function syncAll(force = false) {
@@ -5744,7 +6144,7 @@ import { join as join15 } from "node:path";
5744
6144
  // package.json
5745
6145
  var package_default = {
5746
6146
  name: "agendex-cli",
5747
- version: "2.0.0",
6147
+ version: "2.0.1",
5748
6148
  description: "Agendex CLI for login, sync, and daemon workflows",
5749
6149
  homepage: "https://github.com/Tyru5/Agendex#readme",
5750
6150
  bugs: {
@@ -6304,18 +6704,15 @@ async function main() {
6304
6704
  return 0;
6305
6705
  }
6306
6706
  process.kill(pid, "SIGTERM");
6307
- const deadline = Date.now() + 5000;
6308
- while (isRunning(pid) && Date.now() < deadline) {
6309
- await new Promise((r) => setTimeout(r, 200));
6310
- }
6311
- if (isRunning(pid)) {
6707
+ const stopped = await waitForProcessExit(pid, 5000);
6708
+ if (!stopped) {
6312
6709
  writeStderr("[agendex] daemon did not stop in time");
6313
- } else {
6314
- removePid();
6315
- await sendShutdown();
6316
- writeStdout(`[agendex] daemon stopped (PID ${pid})`);
6710
+ return 1;
6317
6711
  }
6318
- return isRunning(pid) ? 1 : 0;
6712
+ removePid();
6713
+ await sendShutdown();
6714
+ writeStdout(`[agendex] daemon stopped (PID ${pid})`);
6715
+ return 0;
6319
6716
  }
6320
6717
  case "login": {
6321
6718
  const urlIdx = args.indexOf("--url");
@@ -6338,160 +6735,19 @@ async function main() {
6338
6735
  return 0;
6339
6736
  }
6340
6737
  case "upload": {
6341
- return await runUpload(args);
6738
+ return runUpload(args);
6342
6739
  }
6343
6740
  case "hooks": {
6344
- return await runHooksCommand(args, cliEntry);
6741
+ return runHooksCommand(args, cliEntry);
6345
6742
  }
6346
6743
  case "review-plan": {
6347
- return await runHookReviewCommand(args);
6744
+ return runHookReviewCommand(args);
6348
6745
  }
6349
6746
  case "cleanup": {
6350
- const config = loadConfig();
6351
- if (!config?.cloudToken || !config?.convexUrl) {
6352
- writeStderr("[agendex] not logged in. Run `agendex login` first.");
6353
- return 1;
6354
- }
6355
- let allDevices;
6356
- try {
6357
- allDevices = await fetchDevices();
6358
- } catch (err) {
6359
- if (err instanceof AuthExpiredError) {
6360
- writeStderr("[agendex] cloud token expired. Run `agendex login` to re-authenticate.");
6361
- return 1;
6362
- }
6363
- throw err;
6364
- }
6365
- if (allDevices.length === 0) {
6366
- writeStdout("[agendex] no daemons found");
6367
- return 0;
6368
- }
6369
- const now = Date.now();
6370
- const staleDevices = allDevices.filter((d2) => {
6371
- const age = d2.lastSeenAt ? now - d2.lastSeenAt : Number.POSITIVE_INFINITY;
6372
- return age >= CLI_DAEMON_STALE_AFTER_MS;
6373
- });
6374
- if (args.includes("--stale")) {
6375
- if (staleDevices.length === 0) {
6376
- writeStdout("[agendex] no stale daemons to remove");
6377
- return 0;
6378
- }
6379
- const staleIds = staleDevices.map((d2) => d2.deviceId).filter((id) => id != null);
6380
- if (staleIds.length === 0) {
6381
- writeStdout("[agendex] stale daemons have no device IDs and cannot be removed");
6382
- return 0;
6383
- }
6384
- const result2 = await deleteDaemons(staleIds);
6385
- if (result2.ok) {
6386
- writeStdout(`[agendex] removed ${result2.deleted} stale daemon(s)`);
6387
- } else {
6388
- writeStderr("[agendex] failed to remove stale daemons");
6389
- return 1;
6390
- }
6391
- return 0;
6392
- }
6393
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
6394
- writeStderr("[agendex] interactive cleanup requires a TTY. Use --stale to auto-remove stale daemons.");
6395
- return 1;
6396
- }
6397
- const { promptForDaemonCleanup: promptForDaemonCleanup2 } = await Promise.resolve().then(() => (init_cleanup(), exports_cleanup));
6398
- const deviceIds = allDevices.filter((d2) => d2.deviceId != null).map((d2) => {
6399
- const age = d2.lastSeenAt ? now - d2.lastSeenAt : Number.POSITIVE_INFINITY;
6400
- const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
6401
- return {
6402
- deviceId: d2.deviceId,
6403
- hostname: d2.hostname ?? "unknown",
6404
- pid: d2.pid,
6405
- status
6406
- };
6407
- });
6408
- if (deviceIds.length === 0) {
6409
- writeStdout("[agendex] no daemons with device IDs to remove");
6410
- return 0;
6411
- }
6412
- const selected = await promptForDaemonCleanup2(deviceIds);
6413
- if (!selected)
6414
- return 0;
6415
- const result = await deleteDaemons(selected);
6416
- if (result.ok) {
6417
- writeStdout(`[agendex] removed ${result.deleted} daemon(s)`);
6418
- } else {
6419
- writeStderr("[agendex] failed to remove daemons");
6420
- return 1;
6421
- }
6422
- return 0;
6747
+ return runCleanupCommand(args);
6423
6748
  }
6424
6749
  case "add-dir": {
6425
- const dirPath = args.find((a) => a !== "add-dir" && a !== "--dev" && !a.startsWith("--"));
6426
- if (!dirPath || !dirPath.trim()) {
6427
- writeStderr("[agendex] usage: agendex add-dir <path>");
6428
- return 1;
6429
- }
6430
- const resolved = resolveCustomPlanDirPath(dirPath);
6431
- if (!existsSync13(resolved)) {
6432
- writeStderr(`[agendex] path does not exist: ${resolved}`);
6433
- return 1;
6434
- }
6435
- if (!statSync2(resolved).isDirectory()) {
6436
- writeStderr(`[agendex] path is not a directory: ${resolved}`);
6437
- return 1;
6438
- }
6439
- if (args.includes("--live")) {
6440
- const cfg = loadConfig();
6441
- const token = cfg?.token;
6442
- if (!token) {
6443
- writeStderr("[agendex] no local token found in config — is the server running?");
6444
- return 1;
6445
- }
6446
- const port = process.env.PORT ?? "4890";
6447
- const { request } = await import("node:http");
6448
- const body = JSON.stringify({ path: resolved });
6449
- try {
6450
- const res = await new Promise((resolve12, reject) => {
6451
- const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
6452
- method: "POST",
6453
- headers: {
6454
- Authorization: `Bearer ${token}`,
6455
- "Content-Type": "application/json",
6456
- "Content-Length": String(Buffer.byteLength(body))
6457
- }
6458
- }, (res2) => {
6459
- let data = "";
6460
- res2.setEncoding("utf8");
6461
- res2.on("data", (chunk) => {
6462
- data += chunk;
6463
- });
6464
- res2.on("end", () => resolve12({ status: res2.statusCode ?? 0, body: data }));
6465
- res2.on("error", reject);
6466
- });
6467
- req.on("error", reject);
6468
- req.write(body);
6469
- req.end();
6470
- });
6471
- if (res.status >= 200 && res.status < 300) {
6472
- writeStdout(`[agendex] added custom plan dir: ${resolved}`);
6473
- writeStdout(`[agendex] server notified — scanning + watching now`);
6474
- } else {
6475
- writeStderr(`[agendex] server returned ${res.status}: ${res.body}`);
6476
- return 1;
6477
- }
6478
- } catch (err) {
6479
- const msg = err instanceof Error ? err.message : String(err);
6480
- writeStderr(`[agendex] could not reach local server on port ${port}: ${msg}`);
6481
- return 1;
6482
- }
6483
- } else {
6484
- const cfg = loadConfig();
6485
- const currentDirs = cfg?.customPlanDirs ?? [];
6486
- const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
6487
- saveConfig({
6488
- ...cfg ?? { configVersion: 3, enabledAdapters: [] },
6489
- customPlanDirs: updated
6490
- });
6491
- writeStdout(`[agendex] added custom plan dir: ${resolved}`);
6492
- writeStdout(`[agendex] daemon will pick up the change automatically`);
6493
- }
6494
- return 0;
6750
+ return runAddDirCommand(args);
6495
6751
  }
6496
6752
  case "remove-dir": {
6497
6753
  const dirPath = args.find((a) => a !== "remove-dir" && a !== "--dev" && !a.startsWith("--"));
@@ -6532,110 +6788,40 @@ async function main() {
6532
6788
  const pidInfo = readPidInfo();
6533
6789
  const pid = pidInfo?.pid ?? null;
6534
6790
  const running = pid ? isRunning(pid) : false;
6535
- writeStdout(`[agendex] Config version: ${config?.configVersion ?? "none"}`);
6536
- writeStdout(`[agendex] Local token: ${config?.token ? "set" : "not set"}`);
6537
- writeStdout(`[agendex] Cloud token: ${config?.cloudToken ? "set" : "not set"}`);
6538
- writeStdout(`[agendex] Convex URL: ${config?.convexUrl ?? "not set"}`);
6539
- writeStdout(`[agendex] Enabled adapters: ${config?.enabledAdapters.join(", ") || "none"}`);
6540
- const customDirs = config?.customPlanDirs ?? [];
6541
- writeStdout(`[agendex] Custom plan dirs: ${customDirs.length > 0 ? customDirs.length : "none"}`);
6542
- for (const dir of customDirs) {
6543
- writeStdout(` - ${dir}`);
6544
- }
6545
- writeStdout(`[agendex] Daemon: ${running ? `running (PID ${pid})` : "not running"}`);
6546
- if (running && pidInfo?.startedAtMs) {
6547
- writeStdout(`[agendex] Uptime: ${formatDuration(Date.now() - pidInfo.startedAtMs)}`);
6548
- } else if (running) {
6549
- writeStdout(`[agendex] Uptime: unknown (restart daemon to populate)`);
6550
- } else {
6551
- writeStdout(`[agendex] Uptime: n/a`);
6552
- }
6553
- if (running && pidInfo?.hostname) {
6554
- writeStdout(`[agendex] Hostname: ${pidInfo.hostname}`);
6555
- } else if (running) {
6556
- writeStdout(`[agendex] Hostname: unknown (restart daemon to populate)`);
6557
- } else {
6558
- writeStdout(`[agendex] Hostname: n/a`);
6559
- }
6560
- writeStdout(`[agendex] CLI version: ${CLI_VERSION}`);
6561
- try {
6562
- if (config?.cloudToken && config?.convexUrl) {
6563
- const allDevices = await fetchDevices();
6564
- if (allDevices.length > 0) {
6565
- const now = Date.now();
6566
- const localDeviceId = config.deviceId;
6567
- writeStdout(`[agendex] All daemons:`);
6568
- for (const device of allDevices) {
6569
- const age = device.lastSeenAt ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
6570
- const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
6571
- const uptimeStr = device.startedAtMs != null ? formatDuration(now - device.startedAtMs) : "~";
6572
- const pidStr = device.pid != null ? String(device.pid) : "~";
6573
- const hostnameStr = device.hostname ?? "~";
6574
- const ipStr = device.ipAddress ?? "~";
6575
- const isLocal = localDeviceId && device.deviceId === localDeviceId;
6576
- writeStdout(`- hostname: ${hostnameStr}${isLocal ? " (this machine)" : ""}
6577
- ip: ${ipStr}
6578
- pid: ${pidStr}
6579
- uptime: ${uptimeStr}
6580
- status: ${status}`);
6581
- }
6791
+ let devices = null;
6792
+ let cloudDaemonError = null;
6793
+ if (config?.cloudToken && config?.convexUrl) {
6794
+ try {
6795
+ devices = await fetchDevices();
6796
+ } catch (err) {
6797
+ if (err instanceof AuthExpiredError) {
6798
+ cloudDaemonError = { kind: "auth-expired" };
6582
6799
  } else {
6583
- writeStdout(`[agendex] All daemons: none`);
6800
+ cloudDaemonError = {
6801
+ kind: "unavailable",
6802
+ message: err instanceof Error ? err.message : String(err)
6803
+ };
6584
6804
  }
6585
6805
  }
6586
- } catch (err) {
6587
- if (err instanceof AuthExpiredError) {
6588
- writeStderr(`[agendex] Cloud token expired — cloud sync and daemon tracking are inactive.`);
6589
- writeStderr(`[agendex] Run \`agendex login\` to re-authenticate.`);
6590
- }
6591
6806
  }
6807
+ writeStdout(renderStatus({
6808
+ config,
6809
+ configPath: getConfigPath(),
6810
+ pidInfo,
6811
+ running,
6812
+ cliVersion: CLI_VERSION,
6813
+ devices,
6814
+ cloudDaemonError
6815
+ }));
6592
6816
  return 0;
6593
6817
  }
6594
6818
  case "upgrade": {
6595
- return await runUpgrade({ force: args.includes("--force") });
6819
+ return runUpgrade({ force: args.includes("--force") });
6596
6820
  }
6597
6821
  case "help":
6598
6822
  case "--help":
6599
6823
  case "-h": {
6600
- writeStdout(`
6601
- agendex - CLI for syncing local agent plans to the cloud
6602
-
6603
- Usage:
6604
- agendex Start daemon (default, backgrounds itself)
6605
- agendex start Start daemon (backgrounds itself)
6606
- agendex stop Stop the running daemon
6607
- agendex login Authenticate via browser OAuth (agendex.dev)
6608
- agendex login --url <url> Login to a self-hosted instance
6609
- agendex open Open the Agendex web app in your browser
6610
- agendex open --url <url> Open a self-hosted instance
6611
- agendex view <url> Open a shared plan URL in your browser
6612
- agendex logout Clear stored cloud token
6613
- agendex configure Select which agents/adapters to index
6614
- agendex hooks status Show Claude Code, Codex, and Pi hook status
6615
- agendex hooks install <agent|all> Install hook integration (--preview required for claude-code)
6616
- agendex hooks uninstall <agent|all> Remove managed Agendex hook entries
6617
- agendex review-plan --hook --agent <agent> Hook-native plan review command
6618
- agendex add-dir <path> Add a custom directory to scan for plans
6619
- agendex add-dir <path> --live Add dir and notify running server immediately
6620
- agendex remove-dir <path> Remove a custom directory
6621
- agendex list-dirs List custom plan directories
6622
- agendex sync One-shot scan + sync to cloud (skips unchanged plans)
6623
- agendex sync --force Re-sync all plans, ignoring cache
6624
- agendex upload <path> Upload a single Markdown plan file to the cloud
6625
- agendex upload <path> --agent <name> Override the uploaded plan's agent label
6626
- agendex upload <path> --open Open the uploaded plan in the browser after upload
6627
- agendex cleanup Interactively remove cloud daemons
6628
- agendex cleanup --stale Auto-remove all stale daemons
6629
- agendex status Show current config state + daemon status
6630
- agendex upgrade Upgrade the globally installed CLI to the latest version
6631
- agendex upgrade --force Reinstall latest even if already up to date
6632
- agendex help Show this help message
6633
- agendex --version Print CLI version
6634
- agendex -v Print CLI version
6635
-
6636
- Flags:
6637
- --dev Use dev environment (~/.agendex-dev/ config dir)
6638
- `.trim());
6824
+ writeStdout(renderHelp({ cliVersion: CLI_VERSION }));
6639
6825
  return 0;
6640
6826
  }
6641
6827
  default: {
@@ -6645,6 +6831,173 @@ Flags:
6645
6831
  }
6646
6832
  }
6647
6833
  }
6834
+ function waitForProcessExit(pid, timeoutMs) {
6835
+ return new Promise((resolve12) => {
6836
+ const deadline = Date.now() + timeoutMs;
6837
+ const interval = setInterval(() => {
6838
+ if (!isRunning(pid)) {
6839
+ clearInterval(interval);
6840
+ resolve12(true);
6841
+ return;
6842
+ }
6843
+ if (Date.now() >= deadline) {
6844
+ clearInterval(interval);
6845
+ resolve12(false);
6846
+ }
6847
+ }, 200);
6848
+ });
6849
+ }
6850
+ async function runCleanupCommand(commandArgs) {
6851
+ const config = loadConfig();
6852
+ if (!config?.cloudToken || !config?.convexUrl) {
6853
+ writeStderr("[agendex] not logged in. Run `agendex login` first.");
6854
+ return 1;
6855
+ }
6856
+ let allDevices;
6857
+ try {
6858
+ allDevices = await fetchDevices();
6859
+ } catch (err) {
6860
+ if (err instanceof AuthExpiredError) {
6861
+ writeStderr("[agendex] cloud token expired. Run `agendex login` to re-authenticate.");
6862
+ return 1;
6863
+ }
6864
+ throw err;
6865
+ }
6866
+ if (allDevices.length === 0) {
6867
+ writeStdout("[agendex] no daemons found");
6868
+ return 0;
6869
+ }
6870
+ const now = Date.now();
6871
+ const staleDevices = allDevices.filter((device) => {
6872
+ const age = device.lastSeenAt !== null ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
6873
+ return age >= CLI_DAEMON_STALE_AFTER_MS;
6874
+ });
6875
+ if (commandArgs.includes("--stale")) {
6876
+ if (staleDevices.length === 0) {
6877
+ writeStdout("[agendex] no stale daemons to remove");
6878
+ return 0;
6879
+ }
6880
+ const staleIds = staleDevices.flatMap((device) => device.deviceId === null ? [] : [device.deviceId]);
6881
+ if (staleIds.length === 0) {
6882
+ writeStdout("[agendex] stale daemons have no device IDs and cannot be removed");
6883
+ return 0;
6884
+ }
6885
+ const result2 = await deleteDaemons(staleIds);
6886
+ if (result2.ok) {
6887
+ writeStdout(`[agendex] removed ${result2.deleted} stale daemon(s)`);
6888
+ } else {
6889
+ writeStderr("[agendex] failed to remove stale daemons");
6890
+ return 1;
6891
+ }
6892
+ return 0;
6893
+ }
6894
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
6895
+ writeStderr("[agendex] interactive cleanup requires a TTY. Use --stale to auto-remove stale daemons.");
6896
+ return 1;
6897
+ }
6898
+ const { promptForDaemonCleanup: promptForDaemonCleanup2 } = await Promise.resolve().then(() => (init_cleanup(), exports_cleanup));
6899
+ const deviceIds = allDevices.flatMap((device) => {
6900
+ if (device.deviceId === null)
6901
+ return [];
6902
+ const age = device.lastSeenAt !== null ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
6903
+ const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
6904
+ return [
6905
+ {
6906
+ deviceId: device.deviceId,
6907
+ hostname: device.hostname ?? "unknown",
6908
+ pid: device.pid,
6909
+ status
6910
+ }
6911
+ ];
6912
+ });
6913
+ if (deviceIds.length === 0) {
6914
+ writeStdout("[agendex] no daemons with device IDs to remove");
6915
+ return 0;
6916
+ }
6917
+ const selected = await promptForDaemonCleanup2(deviceIds);
6918
+ if (!selected)
6919
+ return 0;
6920
+ const result = await deleteDaemons(selected);
6921
+ if (result.ok) {
6922
+ writeStdout(`[agendex] removed ${result.deleted} daemon(s)`);
6923
+ } else {
6924
+ writeStderr("[agendex] failed to remove daemons");
6925
+ return 1;
6926
+ }
6927
+ return 0;
6928
+ }
6929
+ async function runAddDirCommand(commandArgs) {
6930
+ const dirPath = commandArgs.find((arg) => arg !== "add-dir" && arg !== "--dev" && !arg.startsWith("--"));
6931
+ if (dirPath === undefined || dirPath.trim() === "") {
6932
+ writeStderr("[agendex] usage: agendex add-dir <path>");
6933
+ return 1;
6934
+ }
6935
+ const resolved = resolveCustomPlanDirPath(dirPath);
6936
+ if (!existsSync13(resolved)) {
6937
+ writeStderr(`[agendex] path does not exist: ${resolved}`);
6938
+ return 1;
6939
+ }
6940
+ if (!statSync2(resolved).isDirectory()) {
6941
+ writeStderr(`[agendex] path is not a directory: ${resolved}`);
6942
+ return 1;
6943
+ }
6944
+ if (commandArgs.includes("--live")) {
6945
+ const cfg = loadConfig();
6946
+ const token = cfg?.token;
6947
+ if (!token) {
6948
+ writeStderr("[agendex] no local token found in config — is the server running?");
6949
+ return 1;
6950
+ }
6951
+ const port = process.env.PORT ?? "4890";
6952
+ const { request } = await import("node:http");
6953
+ const body = JSON.stringify({ path: resolved });
6954
+ try {
6955
+ const res = await new Promise((resolve12, reject) => {
6956
+ const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
6957
+ method: "POST",
6958
+ headers: {
6959
+ Authorization: `Bearer ${token}`,
6960
+ "Content-Type": "application/json",
6961
+ "Content-Length": String(Buffer.byteLength(body))
6962
+ }
6963
+ }, (res2) => {
6964
+ let data = "";
6965
+ res2.setEncoding("utf8");
6966
+ res2.on("data", (chunk) => {
6967
+ data += chunk;
6968
+ });
6969
+ res2.on("end", () => resolve12({ status: res2.statusCode ?? 0, body: data }));
6970
+ res2.on("error", reject);
6971
+ });
6972
+ req.on("error", reject);
6973
+ req.write(body);
6974
+ req.end();
6975
+ });
6976
+ if (res.status >= 200 && res.status < 300) {
6977
+ writeStdout(`[agendex] added custom plan dir: ${resolved}`);
6978
+ writeStdout(`[agendex] server notified — scanning + watching now`);
6979
+ } else {
6980
+ writeStderr(`[agendex] server returned ${res.status}: ${res.body}`);
6981
+ return 1;
6982
+ }
6983
+ } catch (err) {
6984
+ const msg = err instanceof Error ? err.message : String(err);
6985
+ writeStderr(`[agendex] could not reach local server on port ${port}: ${msg}`);
6986
+ return 1;
6987
+ }
6988
+ } else {
6989
+ const cfg = loadConfig();
6990
+ const currentDirs = cfg?.customPlanDirs ?? [];
6991
+ const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
6992
+ saveConfig({
6993
+ ...cfg ?? { configVersion: 3, enabledAdapters: [] },
6994
+ customPlanDirs: updated
6995
+ });
6996
+ writeStdout(`[agendex] added custom plan dir: ${resolved}`);
6997
+ writeStdout(`[agendex] daemon will pick up the change automatically`);
6998
+ }
6999
+ return 0;
7000
+ }
6648
7001
  var exitCode = await main().catch((err) => {
6649
7002
  writeStderr(`[agendex] ${err instanceof Error ? err.message : err}`);
6650
7003
  return 1;
@@ -6675,17 +7028,3 @@ function writeStderr(message) {
6675
7028
  writeSync(process.stderr.fd, `${message}
6676
7029
  `);
6677
7030
  }
6678
- function formatDuration(ms) {
6679
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
6680
- const days = Math.floor(totalSeconds / 86400);
6681
- const hours = Math.floor(totalSeconds % 86400 / 3600);
6682
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6683
- const seconds = totalSeconds % 60;
6684
- if (days > 0)
6685
- return `${days}d ${hours}h ${minutes}m ${seconds}s`;
6686
- if (hours > 0)
6687
- return `${hours}h ${minutes}m ${seconds}s`;
6688
- if (minutes > 0)
6689
- return `${minutes}m ${seconds}s`;
6690
- return `${seconds}s`;
6691
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {