agendex-cli 0.16.0 → 0.18.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 +243 -59
  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;
@@ -4137,12 +4176,12 @@ async function login(siteUrlOverride) {
4137
4176
  const existing = loadConfig();
4138
4177
  const config = {
4139
4178
  configVersion: 3,
4140
- token: existing?.token,
4141
4179
  cloudToken: callback.token,
4142
4180
  convexUrl: callback.convexUrl,
4143
- deviceId: existing?.deviceId,
4144
4181
  enabledAdapters: existing?.enabledAdapters ?? [],
4145
- customPlanDirs: existing?.customPlanDirs ?? []
4182
+ customPlanDirs: existing?.customPlanDirs ?? [],
4183
+ ...existing?.token ? { token: existing.token } : {},
4184
+ ...existing?.deviceId ? { deviceId: existing.deviceId } : {}
4146
4185
  };
4147
4186
  saveConfig(config);
4148
4187
  console.log(`[agendex] Logged in successfully!`);
@@ -4156,12 +4195,10 @@ function logout() {
4156
4195
  }
4157
4196
  const config = {
4158
4197
  configVersion: 3,
4159
- token: existing.token,
4160
- cloudToken: undefined,
4161
- convexUrl: undefined,
4162
- deviceId: existing.deviceId,
4163
4198
  enabledAdapters: existing.enabledAdapters,
4164
- customPlanDirs: existing.customPlanDirs
4199
+ customPlanDirs: existing.customPlanDirs,
4200
+ ...existing.token ? { token: existing.token } : {},
4201
+ ...existing.deviceId ? { deviceId: existing.deviceId } : {}
4165
4202
  };
4166
4203
  saveConfig(config);
4167
4204
  console.log("[agendex] Logged out. Cloud token removed.");
@@ -4251,38 +4288,31 @@ async function startCallbackServer() {
4251
4288
  };
4252
4289
  }
4253
4290
  function callbackPage(success) {
4254
- const title = success ? "Login successful" : "Login failed";
4255
- const message = success ? "You can close this tab and return to your terminal." : "Missing token. Please try again.";
4256
- const icon = success ? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:32px;height:32px;color:#22c55e"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:32px;height:32px;color:#ef4444"><path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>';
4291
+ const title = success ? "Signed in" : "Sign in failed";
4292
+ const message = success ? "Return to your terminal." : "Run agendex login again.";
4257
4293
  return `<!DOCTYPE html>
4258
4294
  <html lang="en">
4259
4295
  <head>
4260
4296
  <meta charset="utf-8"/>
4261
4297
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
4262
- <title>${title} Agendex</title>
4298
+ <title>${title} | Agendex</title>
4263
4299
  <style>
4264
- *{margin:0;padding:0;box-sizing:border-box}
4265
- @media(prefers-color-scheme:dark){
4266
- :root{--bg:#111;--surface:#161616;--text:#e8e8e8;--secondary:#888;--tertiary:#555;--border:rgba(255,255,255,0.06)}
4267
- }
4268
- @media(prefers-color-scheme:light){
4269
- :root{--bg:#fafafa;--surface:#fff;--text:#111;--secondary:#666;--tertiary:#999;--border:rgba(0,0,0,0.06)}
4270
- }
4271
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);display:flex;align-items:center;justify-content:center;min-height:100vh;-webkit-font-smoothing:antialiased}
4272
- .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:40px 48px;text-align:center;max-width:400px;width:100%;box-shadow:0 2px 16px rgba(0,0,0,0.04)}
4273
- .icon{margin-bottom:16px;display:flex;justify-content:center}
4274
- h1{font-size:18px;font-weight:600;letter-spacing:-0.02em;margin-bottom:8px}
4275
- p{font-size:13px;color:var(--secondary);line-height:1.5}
4276
- .brand{margin-top:24px;font-size:11px;color:var(--tertiary);letter-spacing:0.04em;font-weight:500}
4300
+ *{box-sizing:border-box}
4301
+ :root{color-scheme:dark light;--bg:oklch(13% 0.018 180);--text:oklch(91% 0.012 125);--muted:oklch(58% 0.018 160);--accent:oklch(90% 0.23 125);--err:oklch(64% 0.2 25)}
4302
+ @media(prefers-color-scheme:light){:root{--bg:oklch(97% 0.014 125);--text:oklch(18% 0.016 135);--muted:oklch(48% 0.018 155)}}
4303
+ body{margin:0;min-height:100vh;background:var(--bg);color:var(--text);font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;display:grid;place-items:center;padding:32px;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
4304
+ main{width:min(100%,340px)}
4305
+ h1{font-size:21px;font-weight:560;line-height:1.25;letter-spacing:-.02em;margin:0}
4306
+ p{font-size:15px;line-height:1.5;color:var(--muted);margin:9px 0 0}
4307
+ .brand{font-family:'SF Mono','JetBrains Mono','Fira Code',ui-monospace,monospace;font-size:12px;line-height:1;color:var(--accent);margin-top:42px;letter-spacing:.02em}
4277
4308
  </style>
4278
4309
  </head>
4279
4310
  <body>
4280
- <div class="card">
4281
- <div class="icon">${icon}</div>
4282
- <h1>${title}</h1>
4311
+ <main aria-labelledby="callback-title">
4312
+ <h1 id="callback-title">${title}</h1>
4283
4313
  <p>${message}</p>
4284
- <div class="brand">AGENDEX</div>
4285
- </div>
4314
+ <div class="brand">agendex</div>
4315
+ </main>
4286
4316
  </body>
4287
4317
  </html>`;
4288
4318
  }
@@ -4311,6 +4341,7 @@ function spawnBrowser(command, args, options = {}) {
4311
4341
 
4312
4342
  // src/daemon.ts
4313
4343
  import { spawn as spawn2 } from "node:child_process";
4344
+ import { hostname as osHostname2 } from "node:os";
4314
4345
  import { resolve as resolve6 } from "node:path";
4315
4346
  import { fileURLToPath } from "node:url";
4316
4347
 
@@ -4340,24 +4371,141 @@ function resolveCliAdapterIds(config) {
4340
4371
  return ids;
4341
4372
  }
4342
4373
 
4374
+ // src/network.ts
4375
+ import { execFileSync } from "node:child_process";
4376
+ import { networkInterfaces } from "node:os";
4377
+ import { platform } from "node:process";
4378
+ var DISABLE_LOCAL_IP_ENV = "AGENDEX_DISABLE_LOCAL_IP";
4379
+ function shouldCollectLocalIpAddress(env = process.env) {
4380
+ const value = env[DISABLE_LOCAL_IP_ENV]?.trim().toLowerCase();
4381
+ return !["1", "true", "yes", "on"].includes(value ?? "");
4382
+ }
4383
+ function getLocalIpAddress(options = {}) {
4384
+ const interfaces = options.interfaces ?? networkInterfaces();
4385
+ const route = options.defaultRoute === undefined ? getDefaultIpv4Route() : options.defaultRoute;
4386
+ const routedAddress = route ? getAddressForRoute(interfaces, route) : undefined;
4387
+ return routedAddress ?? findFirstAddress(interfaces, "IPv4") ?? findFirstAddress(interfaces, "IPv6");
4388
+ }
4389
+ function getAddressForRoute(interfaces, route) {
4390
+ if (route.sourceAddress && isUsableIpv4Address(route.sourceAddress))
4391
+ return route.sourceAddress;
4392
+ if (route.interfaceName)
4393
+ return findFirstAddress(interfaces, "IPv4", route.interfaceName);
4394
+ return;
4395
+ }
4396
+ function findFirstAddress(interfaces, family, interfaceName) {
4397
+ const entries = interfaceName ? [[interfaceName, interfaces[interfaceName]]] : Object.entries(interfaces).sort(([left], [right]) => left.localeCompare(right));
4398
+ for (const [, addrs] of entries) {
4399
+ if (!addrs)
4400
+ continue;
4401
+ for (const addr of addrs) {
4402
+ if (addr.internal)
4403
+ continue;
4404
+ if (addr.family === family)
4405
+ return addr.address;
4406
+ }
4407
+ }
4408
+ return;
4409
+ }
4410
+ function getDefaultIpv4Route() {
4411
+ switch (platform) {
4412
+ case "linux":
4413
+ return parseLinuxRoute(readRouteCommand("ip", ["route", "get", "1.1.1.1"]));
4414
+ case "darwin":
4415
+ case "freebsd":
4416
+ case "netbsd":
4417
+ case "openbsd":
4418
+ return parseBsdRoute(readRouteCommand("route", ["-n", "get", "1.1.1.1"]));
4419
+ case "win32":
4420
+ return getWindowsDefaultRouteAddress();
4421
+ default:
4422
+ return;
4423
+ }
4424
+ }
4425
+ function readRouteCommand(command, args) {
4426
+ try {
4427
+ return execFileSync(command, args, {
4428
+ encoding: "utf8",
4429
+ stdio: ["ignore", "pipe", "ignore"],
4430
+ timeout: 1000
4431
+ });
4432
+ } catch {
4433
+ return;
4434
+ }
4435
+ }
4436
+ function parseLinuxRoute(output) {
4437
+ if (!output)
4438
+ return;
4439
+ return buildRoute({
4440
+ interfaceName: output.match(/\bdev\s+(\S+)/)?.[1],
4441
+ sourceAddress: output.match(/\bsrc\s+(\d{1,3}(?:\.\d{1,3}){3})\b/)?.[1]
4442
+ });
4443
+ }
4444
+ function parseBsdRoute(output) {
4445
+ if (!output)
4446
+ return;
4447
+ return buildRoute({
4448
+ interfaceName: output.match(/^\s*interface:\s+(\S+)/m)?.[1]
4449
+ });
4450
+ }
4451
+ function getWindowsDefaultRouteAddress() {
4452
+ const script = [
4453
+ "$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0'",
4454
+ "| Sort-Object -Property @{ Expression = { $_.RouteMetric + $_.InterfaceMetric } }",
4455
+ "| Select-Object -First 1;",
4456
+ "if ($route) {",
4457
+ "Get-NetIPAddress -AddressFamily IPv4 -InterfaceIndex $route.InterfaceIndex",
4458
+ "| Where-Object { $_.IPAddress -notlike '169.254.*' }",
4459
+ "| Select-Object -First 1 -ExpandProperty IPAddress",
4460
+ "}"
4461
+ ].join(" ");
4462
+ const output = readRouteCommand("powershell.exe", [
4463
+ "-NoProfile",
4464
+ "-NonInteractive",
4465
+ "-Command",
4466
+ script
4467
+ ]);
4468
+ const sourceAddress = output?.split(/\r?\n/).map((line) => line.trim()).find(isUsableIpv4Address);
4469
+ return buildRoute({ sourceAddress });
4470
+ }
4471
+ function buildRoute(route) {
4472
+ return route.interfaceName || route.sourceAddress ? route : undefined;
4473
+ }
4474
+ function isUsableIpv4Address(address) {
4475
+ if (!isIpv4Address(address))
4476
+ return false;
4477
+ return address !== "0.0.0.0" && !address.startsWith("127.");
4478
+ }
4479
+ function isIpv4Address(address) {
4480
+ const octets = address.split(".");
4481
+ return octets.length === 4 && octets.every((octet) => {
4482
+ if (!/^\d+$/.test(octet))
4483
+ return false;
4484
+ const value = Number(octet);
4485
+ return value >= 0 && value <= 255;
4486
+ });
4487
+ }
4488
+
4343
4489
  // src/payload.ts
4344
4490
  var SYNC_METADATA_KEY = "agendexSync";
4345
4491
  function isRecord4(value) {
4346
4492
  return typeof value === "object" && value !== null;
4347
4493
  }
4348
- function withSyncDeviceMetadata(metadata, deviceId) {
4349
- if (!deviceId)
4494
+ function withSyncDeviceMetadata(metadata, deviceId, hostname2, ipAddress) {
4495
+ if (!deviceId && !hostname2 && !ipAddress)
4350
4496
  return metadata;
4351
4497
  const existing = isRecord4(metadata[SYNC_METADATA_KEY]) ? metadata[SYNC_METADATA_KEY] : {};
4352
4498
  return {
4353
4499
  ...metadata,
4354
4500
  [SYNC_METADATA_KEY]: {
4355
4501
  ...existing,
4356
- deviceId
4502
+ ...deviceId !== undefined && { deviceId },
4503
+ ...hostname2 !== undefined && { hostname: hostname2 },
4504
+ ...ipAddress !== undefined && { ipAddress }
4357
4505
  }
4358
4506
  };
4359
4507
  }
4360
- function planToSyncPayload(plan, deviceId) {
4508
+ function planToSyncPayload(plan, deviceId, hostname2, ipAddress) {
4361
4509
  return {
4362
4510
  localPlanId: plan.id,
4363
4511
  agent: plan.agent,
@@ -4366,7 +4514,7 @@ function planToSyncPayload(plan, deviceId) {
4366
4514
  format: plan.format,
4367
4515
  filePath: plan.filePath,
4368
4516
  workspace: plan.workspace,
4369
- metadata: withSyncDeviceMetadata(plan.metadata, deviceId),
4517
+ metadata: withSyncDeviceMetadata(plan.metadata, deviceId, hostname2, ipAddress),
4370
4518
  createdAt: plan.createdAt.getTime(),
4371
4519
  updatedAt: plan.updatedAt.getTime()
4372
4520
  };
@@ -4420,6 +4568,16 @@ function computePayloadHash(payload) {
4420
4568
  return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
4421
4569
  }
4422
4570
 
4571
+ // src/sync-privacy.ts
4572
+ async function shouldIncludeLocalIpAddressInSync() {
4573
+ if (!shouldCollectLocalIpAddress())
4574
+ return false;
4575
+ const prefs = await fetchCliPreferences();
4576
+ if (prefs)
4577
+ return prefs.collectLocalIpAddress;
4578
+ return loadConfig()?.collectLocalIpAddress ?? true;
4579
+ }
4580
+
4423
4581
  // src/writeback-delivery-cache.ts
4424
4582
  import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4425
4583
  import { join as join13 } from "node:path";
@@ -4477,15 +4635,25 @@ var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4477
4635
  var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the request-changes payload.";
4478
4636
  async function runWorker() {
4479
4637
  const config = await loadOrInitConfig();
4638
+ const hostname2 = osHostname2();
4480
4639
  const adapterIds = resolveCliAdapterIds(config);
4481
4640
  const adapters = resolveAdapters(adapterIds);
4482
4641
  setActiveAdapters(adapters);
4483
- console.log(`[agendex] daemon starting with ${adapterIds.length} adapters`);
4484
- await sendHeartbeat();
4485
4642
  const syncCache = loadSyncCache();
4486
4643
  const syncQueue = [];
4487
4644
  const pendingWritebackReports = loadPendingWritebackReports();
4488
4645
  let syncing = false;
4646
+ let cachedIpAddress;
4647
+ async function getSyncIpAddress() {
4648
+ if (!await shouldIncludeLocalIpAddressInSync()) {
4649
+ cachedIpAddress = undefined;
4650
+ return;
4651
+ }
4652
+ cachedIpAddress ??= getLocalIpAddress();
4653
+ return cachedIpAddress;
4654
+ }
4655
+ console.log(`[agendex] daemon starting with ${adapterIds.length} adapters`);
4656
+ await sendHeartbeat(await getSyncIpAddress());
4489
4657
  async function tryRefreshToken() {
4490
4658
  const cfg = loadConfig();
4491
4659
  if (!cfg?.cloudToken || !cfg.convexUrl)
@@ -4600,8 +4768,9 @@ async function runWorker() {
4600
4768
  });
4601
4769
  if (ok) {
4602
4770
  const updatedPlan = getById(job.localPlanId);
4603
- if (updatedPlan)
4604
- syncQueue.push(planToSyncPayload(updatedPlan, config.deviceId));
4771
+ if (updatedPlan) {
4772
+ syncQueue.push(planToSyncPayload(updatedPlan, config.deviceId, hostname2, await getSyncIpAddress()));
4773
+ }
4605
4774
  pendingWritebackReports.set(job._id, "sent");
4606
4775
  persistPendingWritebackReports();
4607
4776
  await reportPendingWriteback(job._id);
@@ -4641,8 +4810,9 @@ async function runWorker() {
4641
4810
  let initialSkipped = 0;
4642
4811
  let initialQueuedSyncable = 0;
4643
4812
  let initialQueuedLowValue = 0;
4813
+ const initialIpAddress = await getSyncIpAddress();
4644
4814
  for (const plan of plans) {
4645
- const payload = planToSyncPayload(plan, config.deviceId);
4815
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, initialIpAddress);
4646
4816
  const hash = computePayloadHash(payload);
4647
4817
  if (syncCache[plan.id] === hash) {
4648
4818
  initialSkipped++;
@@ -4664,16 +4834,25 @@ async function runWorker() {
4664
4834
  const lowValueSuffix = lowValuePlanCount > 0 ? `, ${lowValuePlanCount} low-value hidden/pruned` : "";
4665
4835
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
4666
4836
  await processSyncQueue();
4667
- setInterval(() => void sendHeartbeat(), CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
4837
+ setInterval(() => {
4838
+ (async () => {
4839
+ await sendHeartbeat(await getSyncIpAddress());
4840
+ })().catch(() => {});
4841
+ }, CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
4668
4842
  if (shouldEnablePlannotatorSync(config)) {
4669
4843
  setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4670
4844
  pollPlannotatorWritebacks();
4671
4845
  }
4672
4846
  startWatching((changedPlans) => {
4673
- for (const plan of changedPlans) {
4674
- syncQueue.push(planToSyncPayload(plan, config.deviceId));
4675
- }
4676
- processSyncQueue();
4847
+ (async () => {
4848
+ const ipAddress = await getSyncIpAddress();
4849
+ for (const plan of changedPlans) {
4850
+ syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4851
+ }
4852
+ processSyncQueue();
4853
+ })().catch((err) => {
4854
+ console.error("[agendex] failed to queue changed plans:", err);
4855
+ });
4677
4856
  });
4678
4857
  console.log(`[agendex] daemon running. Watching for file changes...`);
4679
4858
  async function gracefulShutdown() {
@@ -4734,8 +4913,11 @@ async function startSupervisor() {
4734
4913
  }
4735
4914
 
4736
4915
  // src/sync.ts
4916
+ import { hostname as osHostname3 } from "node:os";
4737
4917
  async function syncAll(force = false) {
4738
4918
  const config = await loadOrInitConfig();
4919
+ const hostname2 = osHostname3();
4920
+ const ipAddress = await shouldIncludeLocalIpAddressInSync() ? getLocalIpAddress() : undefined;
4739
4921
  const adapterIds = resolveCliAdapterIds(config);
4740
4922
  const adapters = resolveAdapters(adapterIds);
4741
4923
  setActiveAdapters(adapters);
@@ -4755,7 +4937,7 @@ async function syncAll(force = false) {
4755
4937
  let failed = 0;
4756
4938
  for (const plan of [...syncablePlans, ...lowValuePlans]) {
4757
4939
  activePlanIds.add(plan.id);
4758
- const payload = planToSyncPayload(plan, config.deviceId);
4940
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, ipAddress);
4759
4941
  const hash = computePayloadHash(payload);
4760
4942
  if (!force && cache[plan.id] === hash) {
4761
4943
  skipped++;
@@ -4798,19 +4980,18 @@ import { join as join14 } from "node:path";
4798
4980
  // package.json
4799
4981
  var package_default = {
4800
4982
  name: "agendex-cli",
4801
- version: "0.16.0",
4983
+ version: "0.18.0",
4802
4984
  description: "Agendex CLI for login, sync, and daemon workflows",
4803
4985
  homepage: "https://github.com/Tyru5/Agendex#readme",
4986
+ bugs: {
4987
+ url: "https://github.com/Tyru5/Agendex/issues"
4988
+ },
4989
+ license: "AGPL-3.0-only",
4804
4990
  repository: {
4805
4991
  type: "git",
4806
4992
  url: "git+https://github.com/Tyru5/Agendex.git",
4807
4993
  directory: "packages/cli"
4808
4994
  },
4809
- bugs: {
4810
- url: "https://github.com/Tyru5/Agendex/issues"
4811
- },
4812
- type: "module",
4813
- license: "AGPL-3.0-only",
4814
4995
  bin: {
4815
4996
  agendex: "./dist/cli.js"
4816
4997
  },
@@ -4819,12 +5000,10 @@ var package_default = {
4819
5000
  "README.md",
4820
5001
  "LICENSE"
4821
5002
  ],
5003
+ type: "module",
4822
5004
  publishConfig: {
4823
5005
  access: "public"
4824
5006
  },
4825
- engines: {
4826
- node: ">=20"
4827
- },
4828
5007
  scripts: {
4829
5008
  build: "node ./scripts/build-release.mjs --dist-only",
4830
5009
  "build:release": "node ./scripts/build-release.mjs",
@@ -4837,6 +5016,9 @@ var package_default = {
4837
5016
  devDependencies: {
4838
5017
  "@agendex/shared": "workspace:*",
4839
5018
  "@types/bun": "^1.3.9"
5019
+ },
5020
+ engines: {
5021
+ node: ">=20"
4840
5022
  }
4841
5023
  };
4842
5024
 
@@ -5485,8 +5667,10 @@ async function main() {
5485
5667
  const uptimeStr = device.startedAtMs != null ? formatDuration(now - device.startedAtMs) : "~";
5486
5668
  const pidStr = device.pid != null ? String(device.pid) : "~";
5487
5669
  const hostnameStr = device.hostname ?? "~";
5670
+ const ipStr = device.ipAddress ?? "~";
5488
5671
  const isLocal = localDeviceId && device.deviceId === localDeviceId;
5489
5672
  writeStdout(`- hostname: ${hostnameStr}${isLocal ? " (this machine)" : ""}
5673
+ ip: ${ipStr}
5490
5674
  pid: ${pidStr}
5491
5675
  uptime: ${uptimeStr}
5492
5676
  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.18.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {