agendex-cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/cli.js +222 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,6 +57,16 @@ AGENDEX_DEV=1 agendex sync
57
57
 
58
58
  In dev mode the default OAuth site (when you do not pass `--url` and do not set `AGENDEX_SITE_URL`) points at the local EE app URL used for development.
59
59
 
60
+ ## Sync Provenance
61
+
62
+ `agendex sync` and the daemon include sync provenance in cloud payload metadata so the web app can show where a plan was synced from. This includes the device ID, hostname, and the host machine's local IP address when one is available.
63
+
64
+ You can disable local IP address collection from Account settings in the cloud app. Managed or non-interactive environments can also omit the local IP address from sync payloads by setting:
65
+
66
+ ```bash
67
+ AGENDEX_DISABLE_LOCAL_IP=1 agendex sync
68
+ ```
69
+
60
70
  ## Daemon Cleanup
61
71
 
62
72
  `agendex cleanup` manages registered daemon devices in the cloud.
package/dist/cli.js CHANGED
@@ -2797,12 +2797,14 @@ function normalizeStoredConfig(raw) {
2797
2797
  const cloudToken = typeof raw.cloudToken === "string" && raw.cloudToken.trim() ? raw.cloudToken : undefined;
2798
2798
  const convexUrl = typeof raw.convexUrl === "string" && raw.convexUrl.trim() ? raw.convexUrl : undefined;
2799
2799
  const deviceId = typeof raw.deviceId === "string" && raw.deviceId.trim() ? raw.deviceId : undefined;
2800
+ const collectLocalIpAddress = typeof raw.collectLocalIpAddress === "boolean" ? raw.collectLocalIpAddress : undefined;
2800
2801
  return {
2801
2802
  configVersion: 3,
2802
2803
  token,
2803
2804
  cloudToken,
2804
2805
  convexUrl,
2805
2806
  deviceId,
2807
+ collectLocalIpAddress,
2806
2808
  enabledAdapters: normalizeAdapterIds(raw.enabledAdapters),
2807
2809
  customPlanDirs: normalizeCustomPlanDirs(raw.customPlanDirs)
2808
2810
  };
@@ -2818,6 +2820,7 @@ function saveConfig(config) {
2818
2820
  cloudToken: config.cloudToken,
2819
2821
  convexUrl: config.convexUrl,
2820
2822
  deviceId: config.deviceId,
2823
+ collectLocalIpAddress: config.collectLocalIpAddress,
2821
2824
  enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters),
2822
2825
  customPlanDirs: normalizeCustomPlanDirs(config.customPlanDirs)
2823
2826
  };
@@ -3150,7 +3153,7 @@ function looksLikeExecutionReport(normalized) {
3150
3153
  const hasPastCompletion = /\b(?:fixed|pushed|committed|completed|done|implemented|updated|changed|patched|merged|deployed|passed|failed|resolved|reverted)\b/i.test(normalized);
3151
3154
  const hasReportSection = /^\s*(?:summary|result|results|changes|verification|status)\s*:/im.test(normalized);
3152
3155
  const hasReviewReportMarker = /\b(?:review findings?|review issues?|review comments?)\b/i.test(normalized);
3153
- const hasCommandMarker = /::[a-z0-9_-]+(?:\{|\[|\s*$)/im.test(normalized) || /`[^`]*(?:bun|npm|pnpm|yarn|git|tsc|biome)[^`]*`/i.test(normalized) || /\b(?:git\s+(?:stage|commit|push|status)|bunx?\s+|npm\s+|pnpm\s+|yarn\s+)\b/i.test(normalized);
3156
+ const hasCommandMarker = /::[a-z0-9_-]+(?:\{|\[|\s*$)/im.test(normalized) || /`[^`]*(?:bun|npm|pnpm|yarn|git|tsc|oxfmt|oxlint|biome)[^`]*`/i.test(normalized) || /\b(?:git\s+(?:stage|commit|push|status)|bunx?\s+|npm\s+|pnpm\s+|yarn\s+)\b/i.test(normalized);
3154
3157
  return hasPastCompletion && (hasReportSection || hasCommandMarker || hasReviewReportMarker);
3155
3158
  }
3156
3159
  function lowValueAssessment(reasons, signals) {
@@ -3882,7 +3885,7 @@ async function refreshStoredToken(currentToken, convexUrl) {
3882
3885
  }
3883
3886
  return refreshed.token;
3884
3887
  }
3885
- async function sendHeartbeat() {
3888
+ async function sendHeartbeat(ipAddress) {
3886
3889
  try {
3887
3890
  const { token, convexUrl } = getCloudConfig();
3888
3891
  const pidInfo = readPidInfo();
@@ -3891,7 +3894,8 @@ async function sendHeartbeat() {
3891
3894
  deviceId: cachedDeviceId,
3892
3895
  hostname: pidInfo?.hostname ?? osHostname(),
3893
3896
  startedAtMs: pidInfo?.startedAtMs,
3894
- pid: pidInfo?.pid
3897
+ pid: pidInfo?.pid,
3898
+ ipAddress: ipAddress ?? null
3895
3899
  });
3896
3900
  let activeToken = token;
3897
3901
  let res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
@@ -3946,6 +3950,41 @@ async function refreshToken(currentToken, convexUrl) {
3946
3950
  return null;
3947
3951
  return { token: body.token, expiresAt: body.expiresAt ?? 0 };
3948
3952
  }
3953
+ async function fetchCliPreferences() {
3954
+ try {
3955
+ const { token, convexUrl } = getCloudConfig();
3956
+ let activeToken = token;
3957
+ let res = await requestText(`${convexUrl}/api/cli/preferences`, {
3958
+ method: "GET",
3959
+ headers: authHeaders(activeToken)
3960
+ });
3961
+ if (res.status === 401) {
3962
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
3963
+ if (refreshed) {
3964
+ activeToken = refreshed;
3965
+ res = await requestText(`${convexUrl}/api/cli/preferences`, {
3966
+ method: "GET",
3967
+ headers: authHeaders(activeToken)
3968
+ });
3969
+ }
3970
+ }
3971
+ if (res.status < 200 || res.status >= 300)
3972
+ return null;
3973
+ const body = JSON.parse(res.body);
3974
+ if (typeof body.collectLocalIpAddress !== "boolean")
3975
+ return null;
3976
+ const config = loadConfig();
3977
+ if (config) {
3978
+ saveConfig({
3979
+ ...config,
3980
+ collectLocalIpAddress: body.collectLocalIpAddress
3981
+ });
3982
+ }
3983
+ return { collectLocalIpAddress: body.collectLocalIpAddress };
3984
+ } catch {
3985
+ return null;
3986
+ }
3987
+ }
3949
3988
  var REQUEST_TIMEOUT_MS2 = Number.parseInt(process.env.AGENDEX_HTTP_TIMEOUT_MS ?? "", 10) || 1e4;
3950
3989
  function requestText(urlString, options) {
3951
3990
  const url = new URL(urlString);
@@ -4112,7 +4151,7 @@ async function deleteDaemons(deviceIds) {
4112
4151
  import { spawn } from "node:child_process";
4113
4152
  import { createServer } from "node:http";
4114
4153
  var PROD_SITE_URL = "https://app.agendex.dev";
4115
- var DEV_SITE_URL = "http://app.agendex.local:5174";
4154
+ var DEV_SITE_URL = "http://app.agendex.localhost:5174";
4116
4155
  function getDefaultSiteUrl() {
4117
4156
  if (process.env.AGENDEX_SITE_URL)
4118
4157
  return process.env.AGENDEX_SITE_URL;
@@ -4311,6 +4350,7 @@ function spawnBrowser(command, args, options = {}) {
4311
4350
 
4312
4351
  // src/daemon.ts
4313
4352
  import { spawn as spawn2 } from "node:child_process";
4353
+ import { hostname as osHostname2 } from "node:os";
4314
4354
  import { resolve as resolve6 } from "node:path";
4315
4355
  import { fileURLToPath } from "node:url";
4316
4356
 
@@ -4340,24 +4380,141 @@ function resolveCliAdapterIds(config) {
4340
4380
  return ids;
4341
4381
  }
4342
4382
 
4383
+ // src/network.ts
4384
+ import { execFileSync } from "node:child_process";
4385
+ import { networkInterfaces } from "node:os";
4386
+ import { platform } from "node:process";
4387
+ var DISABLE_LOCAL_IP_ENV = "AGENDEX_DISABLE_LOCAL_IP";
4388
+ function shouldCollectLocalIpAddress(env = process.env) {
4389
+ const value = env[DISABLE_LOCAL_IP_ENV]?.trim().toLowerCase();
4390
+ return !["1", "true", "yes", "on"].includes(value ?? "");
4391
+ }
4392
+ function getLocalIpAddress(options = {}) {
4393
+ const interfaces = options.interfaces ?? networkInterfaces();
4394
+ const route = options.defaultRoute === undefined ? getDefaultIpv4Route() : options.defaultRoute;
4395
+ const routedAddress = route ? getAddressForRoute(interfaces, route) : undefined;
4396
+ return routedAddress ?? findFirstAddress(interfaces, "IPv4") ?? findFirstAddress(interfaces, "IPv6");
4397
+ }
4398
+ function getAddressForRoute(interfaces, route) {
4399
+ if (route.sourceAddress && isUsableIpv4Address(route.sourceAddress))
4400
+ return route.sourceAddress;
4401
+ if (route.interfaceName)
4402
+ return findFirstAddress(interfaces, "IPv4", route.interfaceName);
4403
+ return;
4404
+ }
4405
+ function findFirstAddress(interfaces, family, interfaceName) {
4406
+ const entries = interfaceName ? [[interfaceName, interfaces[interfaceName]]] : Object.entries(interfaces).sort(([left], [right]) => left.localeCompare(right));
4407
+ for (const [, addrs] of entries) {
4408
+ if (!addrs)
4409
+ continue;
4410
+ for (const addr of addrs) {
4411
+ if (addr.internal)
4412
+ continue;
4413
+ if (addr.family === family)
4414
+ return addr.address;
4415
+ }
4416
+ }
4417
+ return;
4418
+ }
4419
+ function getDefaultIpv4Route() {
4420
+ switch (platform) {
4421
+ case "linux":
4422
+ return parseLinuxRoute(readRouteCommand("ip", ["route", "get", "1.1.1.1"]));
4423
+ case "darwin":
4424
+ case "freebsd":
4425
+ case "netbsd":
4426
+ case "openbsd":
4427
+ return parseBsdRoute(readRouteCommand("route", ["-n", "get", "1.1.1.1"]));
4428
+ case "win32":
4429
+ return getWindowsDefaultRouteAddress();
4430
+ default:
4431
+ return;
4432
+ }
4433
+ }
4434
+ function readRouteCommand(command, args) {
4435
+ try {
4436
+ return execFileSync(command, args, {
4437
+ encoding: "utf8",
4438
+ stdio: ["ignore", "pipe", "ignore"],
4439
+ timeout: 1000
4440
+ });
4441
+ } catch {
4442
+ return;
4443
+ }
4444
+ }
4445
+ function parseLinuxRoute(output) {
4446
+ if (!output)
4447
+ return;
4448
+ return buildRoute({
4449
+ interfaceName: output.match(/\bdev\s+(\S+)/)?.[1],
4450
+ sourceAddress: output.match(/\bsrc\s+(\d{1,3}(?:\.\d{1,3}){3})\b/)?.[1]
4451
+ });
4452
+ }
4453
+ function parseBsdRoute(output) {
4454
+ if (!output)
4455
+ return;
4456
+ return buildRoute({
4457
+ interfaceName: output.match(/^\s*interface:\s+(\S+)/m)?.[1]
4458
+ });
4459
+ }
4460
+ function getWindowsDefaultRouteAddress() {
4461
+ const script = [
4462
+ "$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0'",
4463
+ "| Sort-Object -Property @{ Expression = { $_.RouteMetric + $_.InterfaceMetric } }",
4464
+ "| Select-Object -First 1;",
4465
+ "if ($route) {",
4466
+ "Get-NetIPAddress -AddressFamily IPv4 -InterfaceIndex $route.InterfaceIndex",
4467
+ "| Where-Object { $_.IPAddress -notlike '169.254.*' }",
4468
+ "| Select-Object -First 1 -ExpandProperty IPAddress",
4469
+ "}"
4470
+ ].join(" ");
4471
+ const output = readRouteCommand("powershell.exe", [
4472
+ "-NoProfile",
4473
+ "-NonInteractive",
4474
+ "-Command",
4475
+ script
4476
+ ]);
4477
+ const sourceAddress = output?.split(/\r?\n/).map((line) => line.trim()).find(isUsableIpv4Address);
4478
+ return buildRoute({ sourceAddress });
4479
+ }
4480
+ function buildRoute(route) {
4481
+ return route.interfaceName || route.sourceAddress ? route : undefined;
4482
+ }
4483
+ function isUsableIpv4Address(address) {
4484
+ if (!isIpv4Address(address))
4485
+ return false;
4486
+ return address !== "0.0.0.0" && !address.startsWith("127.");
4487
+ }
4488
+ function isIpv4Address(address) {
4489
+ const octets = address.split(".");
4490
+ return octets.length === 4 && octets.every((octet) => {
4491
+ if (!/^\d+$/.test(octet))
4492
+ return false;
4493
+ const value = Number(octet);
4494
+ return value >= 0 && value <= 255;
4495
+ });
4496
+ }
4497
+
4343
4498
  // src/payload.ts
4344
4499
  var SYNC_METADATA_KEY = "agendexSync";
4345
4500
  function isRecord4(value) {
4346
4501
  return typeof value === "object" && value !== null;
4347
4502
  }
4348
- function withSyncDeviceMetadata(metadata, deviceId) {
4349
- if (!deviceId)
4503
+ function withSyncDeviceMetadata(metadata, deviceId, hostname2, ipAddress) {
4504
+ if (!deviceId && !hostname2 && !ipAddress)
4350
4505
  return metadata;
4351
4506
  const existing = isRecord4(metadata[SYNC_METADATA_KEY]) ? metadata[SYNC_METADATA_KEY] : {};
4352
4507
  return {
4353
4508
  ...metadata,
4354
4509
  [SYNC_METADATA_KEY]: {
4355
4510
  ...existing,
4356
- deviceId
4511
+ ...deviceId !== undefined && { deviceId },
4512
+ ...hostname2 !== undefined && { hostname: hostname2 },
4513
+ ...ipAddress !== undefined && { ipAddress }
4357
4514
  }
4358
4515
  };
4359
4516
  }
4360
- function planToSyncPayload(plan, deviceId) {
4517
+ function planToSyncPayload(plan, deviceId, hostname2, ipAddress) {
4361
4518
  return {
4362
4519
  localPlanId: plan.id,
4363
4520
  agent: plan.agent,
@@ -4366,7 +4523,7 @@ function planToSyncPayload(plan, deviceId) {
4366
4523
  format: plan.format,
4367
4524
  filePath: plan.filePath,
4368
4525
  workspace: plan.workspace,
4369
- metadata: withSyncDeviceMetadata(plan.metadata, deviceId),
4526
+ metadata: withSyncDeviceMetadata(plan.metadata, deviceId, hostname2, ipAddress),
4370
4527
  createdAt: plan.createdAt.getTime(),
4371
4528
  updatedAt: plan.updatedAt.getTime()
4372
4529
  };
@@ -4420,6 +4577,16 @@ function computePayloadHash(payload) {
4420
4577
  return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
4421
4578
  }
4422
4579
 
4580
+ // src/sync-privacy.ts
4581
+ async function shouldIncludeLocalIpAddressInSync() {
4582
+ if (!shouldCollectLocalIpAddress())
4583
+ return false;
4584
+ const prefs = await fetchCliPreferences();
4585
+ if (prefs)
4586
+ return prefs.collectLocalIpAddress;
4587
+ return loadConfig()?.collectLocalIpAddress ?? true;
4588
+ }
4589
+
4423
4590
  // src/writeback-delivery-cache.ts
4424
4591
  import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4425
4592
  import { join as join13 } from "node:path";
@@ -4477,15 +4644,25 @@ var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4477
4644
  var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the request-changes payload.";
4478
4645
  async function runWorker() {
4479
4646
  const config = await loadOrInitConfig();
4647
+ const hostname2 = osHostname2();
4480
4648
  const adapterIds = resolveCliAdapterIds(config);
4481
4649
  const adapters = resolveAdapters(adapterIds);
4482
4650
  setActiveAdapters(adapters);
4483
- console.log(`[agendex] daemon starting with ${adapterIds.length} adapters`);
4484
- await sendHeartbeat();
4485
4651
  const syncCache = loadSyncCache();
4486
4652
  const syncQueue = [];
4487
4653
  const pendingWritebackReports = loadPendingWritebackReports();
4488
4654
  let syncing = false;
4655
+ let cachedIpAddress;
4656
+ async function getSyncIpAddress() {
4657
+ if (!await shouldIncludeLocalIpAddressInSync()) {
4658
+ cachedIpAddress = undefined;
4659
+ return;
4660
+ }
4661
+ cachedIpAddress ??= getLocalIpAddress();
4662
+ return cachedIpAddress;
4663
+ }
4664
+ console.log(`[agendex] daemon starting with ${adapterIds.length} adapters`);
4665
+ await sendHeartbeat(await getSyncIpAddress());
4489
4666
  async function tryRefreshToken() {
4490
4667
  const cfg = loadConfig();
4491
4668
  if (!cfg?.cloudToken || !cfg.convexUrl)
@@ -4600,8 +4777,9 @@ async function runWorker() {
4600
4777
  });
4601
4778
  if (ok) {
4602
4779
  const updatedPlan = getById(job.localPlanId);
4603
- if (updatedPlan)
4604
- syncQueue.push(planToSyncPayload(updatedPlan, config.deviceId));
4780
+ if (updatedPlan) {
4781
+ syncQueue.push(planToSyncPayload(updatedPlan, config.deviceId, hostname2, await getSyncIpAddress()));
4782
+ }
4605
4783
  pendingWritebackReports.set(job._id, "sent");
4606
4784
  persistPendingWritebackReports();
4607
4785
  await reportPendingWriteback(job._id);
@@ -4641,8 +4819,9 @@ async function runWorker() {
4641
4819
  let initialSkipped = 0;
4642
4820
  let initialQueuedSyncable = 0;
4643
4821
  let initialQueuedLowValue = 0;
4822
+ const initialIpAddress = await getSyncIpAddress();
4644
4823
  for (const plan of plans) {
4645
- const payload = planToSyncPayload(plan, config.deviceId);
4824
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, initialIpAddress);
4646
4825
  const hash = computePayloadHash(payload);
4647
4826
  if (syncCache[plan.id] === hash) {
4648
4827
  initialSkipped++;
@@ -4664,16 +4843,25 @@ async function runWorker() {
4664
4843
  const lowValueSuffix = lowValuePlanCount > 0 ? `, ${lowValuePlanCount} low-value hidden/pruned` : "";
4665
4844
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
4666
4845
  await processSyncQueue();
4667
- setInterval(() => void sendHeartbeat(), CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
4846
+ setInterval(() => {
4847
+ (async () => {
4848
+ await sendHeartbeat(await getSyncIpAddress());
4849
+ })().catch(() => {});
4850
+ }, CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
4668
4851
  if (shouldEnablePlannotatorSync(config)) {
4669
4852
  setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4670
4853
  pollPlannotatorWritebacks();
4671
4854
  }
4672
4855
  startWatching((changedPlans) => {
4673
- for (const plan of changedPlans) {
4674
- syncQueue.push(planToSyncPayload(plan, config.deviceId));
4675
- }
4676
- processSyncQueue();
4856
+ (async () => {
4857
+ const ipAddress = await getSyncIpAddress();
4858
+ for (const plan of changedPlans) {
4859
+ syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4860
+ }
4861
+ processSyncQueue();
4862
+ })().catch((err) => {
4863
+ console.error("[agendex] failed to queue changed plans:", err);
4864
+ });
4677
4865
  });
4678
4866
  console.log(`[agendex] daemon running. Watching for file changes...`);
4679
4867
  async function gracefulShutdown() {
@@ -4734,8 +4922,11 @@ async function startSupervisor() {
4734
4922
  }
4735
4923
 
4736
4924
  // src/sync.ts
4925
+ import { hostname as osHostname3 } from "node:os";
4737
4926
  async function syncAll(force = false) {
4738
4927
  const config = await loadOrInitConfig();
4928
+ const hostname2 = osHostname3();
4929
+ const ipAddress = await shouldIncludeLocalIpAddressInSync() ? getLocalIpAddress() : undefined;
4739
4930
  const adapterIds = resolveCliAdapterIds(config);
4740
4931
  const adapters = resolveAdapters(adapterIds);
4741
4932
  setActiveAdapters(adapters);
@@ -4755,7 +4946,7 @@ async function syncAll(force = false) {
4755
4946
  let failed = 0;
4756
4947
  for (const plan of [...syncablePlans, ...lowValuePlans]) {
4757
4948
  activePlanIds.add(plan.id);
4758
- const payload = planToSyncPayload(plan, config.deviceId);
4949
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, ipAddress);
4759
4950
  const hash = computePayloadHash(payload);
4760
4951
  if (!force && cache[plan.id] === hash) {
4761
4952
  skipped++;
@@ -4798,19 +4989,18 @@ import { join as join14 } from "node:path";
4798
4989
  // package.json
4799
4990
  var package_default = {
4800
4991
  name: "agendex-cli",
4801
- version: "0.16.0",
4992
+ version: "0.17.0",
4802
4993
  description: "Agendex CLI for login, sync, and daemon workflows",
4803
4994
  homepage: "https://github.com/Tyru5/Agendex#readme",
4995
+ bugs: {
4996
+ url: "https://github.com/Tyru5/Agendex/issues"
4997
+ },
4998
+ license: "AGPL-3.0-only",
4804
4999
  repository: {
4805
5000
  type: "git",
4806
5001
  url: "git+https://github.com/Tyru5/Agendex.git",
4807
5002
  directory: "packages/cli"
4808
5003
  },
4809
- bugs: {
4810
- url: "https://github.com/Tyru5/Agendex/issues"
4811
- },
4812
- type: "module",
4813
- license: "AGPL-3.0-only",
4814
5004
  bin: {
4815
5005
  agendex: "./dist/cli.js"
4816
5006
  },
@@ -4819,12 +5009,10 @@ var package_default = {
4819
5009
  "README.md",
4820
5010
  "LICENSE"
4821
5011
  ],
5012
+ type: "module",
4822
5013
  publishConfig: {
4823
5014
  access: "public"
4824
5015
  },
4825
- engines: {
4826
- node: ">=20"
4827
- },
4828
5016
  scripts: {
4829
5017
  build: "node ./scripts/build-release.mjs --dist-only",
4830
5018
  "build:release": "node ./scripts/build-release.mjs",
@@ -4837,6 +5025,9 @@ var package_default = {
4837
5025
  devDependencies: {
4838
5026
  "@agendex/shared": "workspace:*",
4839
5027
  "@types/bun": "^1.3.9"
5028
+ },
5029
+ engines: {
5030
+ node: ">=20"
4840
5031
  }
4841
5032
  };
4842
5033
 
@@ -5485,8 +5676,10 @@ async function main() {
5485
5676
  const uptimeStr = device.startedAtMs != null ? formatDuration(now - device.startedAtMs) : "~";
5486
5677
  const pidStr = device.pid != null ? String(device.pid) : "~";
5487
5678
  const hostnameStr = device.hostname ?? "~";
5679
+ const ipStr = device.ipAddress ?? "~";
5488
5680
  const isLocal = localDeviceId && device.deviceId === localDeviceId;
5489
5681
  writeStdout(`- hostname: ${hostnameStr}${isLocal ? " (this machine)" : ""}
5682
+ ip: ${ipStr}
5490
5683
  pid: ${pidStr}
5491
5684
  uptime: ${uptimeStr}
5492
5685
  status: ${status}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {