agendex-cli 1.2.0 → 1.4.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 +44 -0
  2. package/dist/cli.js +357 -98
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,7 @@ agendex configure # Select which agents/adapters to index
23
23
  agendex start # Start daemon (backgrounds itself)
24
24
  agendex stop # Stop the running daemon
25
25
  agendex sync # One-shot scan + sync to cloud
26
+ agendex sync --force # Re-sync all plans, ignoring the local hash cache
26
27
  agendex upload <path> # Upload a single Markdown plan file to the cloud
27
28
  agendex upload <path> --agent <name> # Override the uploaded plan's agent label
28
29
  agendex upload <path> --open # Open the uploaded plan in the browser after upload
@@ -60,6 +61,27 @@ AGENDEX_DEV=1 agendex sync
60
61
 
61
62
  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.
62
63
 
64
+ ## Plan Value Filtering
65
+
66
+ Agendex uses a shared plan-value classifier (`@agendex/shared`) to keep non-plans out of your library and cloud account. The same rules apply to local OSS indexing, `agendex sync`, and the background daemon.
67
+
68
+ **Locally indexed but hidden** (tagged `lowValue` in plan metadata, excluded from search and list views):
69
+
70
+ - Empty or whitespace-only content
71
+ - Heading-only markdown with no body
72
+ - Prompt-like one-liners, system context dumps, tool logs, conversation artifacts
73
+ - Execution reports, review output, wrapper titles
74
+ - Code-only or code-dominated markdown without plan structure
75
+ - Content with no recognizable planning signals (unstructured one-liners, generic session dumps)
76
+
77
+ **Cloud sync behavior:**
78
+
79
+ - Indexable plans upload normally.
80
+ - Low-value plans are still sent on sync so the cloud can **prune** them: existing cloud copies are deleted and new low-value uploads are skipped.
81
+ - Sync output includes counts such as `N low-value skipped/pruned (M deleted)` when pruning runs.
82
+
83
+ Low-value tagging happens during scan/rescan. If you edit a file into a real plan, the next scan clears the tag and sync uploads it again. Version restore in the cloud rejects low-value snapshots; browse history on a hidden plan to find and restore a good snapshot.
84
+
63
85
  ## Sync Provenance
64
86
 
65
87
  `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.
@@ -70,6 +92,28 @@ You can disable local IP address collection from Account settings in the cloud a
70
92
  AGENDEX_DISABLE_LOCAL_IP=1 agendex sync
71
93
  ```
72
94
 
95
+ ## Real-Time Cloud Sync (Daemon)
96
+
97
+ While `agendex start` is running, the daemon watches local plan sources and uploads changes to your cloud account. The cloud web app updates reactively once uploads land (no manual refresh).
98
+
99
+ **How uploads are scheduled:**
100
+
101
+ 1. File watchers (plus periodic rescans) trigger a local rescan when plans change.
102
+ 2. Each changed plan is converted to a sync payload and enqueued. The queue **deduplicates by plan id** (last write wins).
103
+ 3. **`sync-cache.json`** stores content hashes so unchanged plans are skipped (same as one-shot sync). Use `agendex sync --force` to bypass the cache for a manual full upload.
104
+ 4. Before each upload, the daemon re-checks that the queued payload is still the latest edit for that plan (so a slow retry cannot overwrite a newer change).
105
+ 5. Failed uploads retry automatically with exponential backoff (**2s → 8s → 30s**, up to three attempts) before the daemon logs a permanent failure.
106
+
107
+ Low-value plans follow the same queue and pruning rules as [Plan Value Filtering](#plan-value-filtering).
108
+
109
+ | Variable | Default | Purpose |
110
+ | ------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
111
+ | `AGENDEX_LIVE_SESSION_POLL_MS` | `2000` | Poll active Plannotator live sessions (re-fetch loopback plan content). Set to `0` to disable. |
112
+ | `AGENDEX_SYNC_RESCAN_INTERVAL_MS` | `60000` | Safety-net full rescan + hash-diff upload when `fs.watch` misses an event. Set to `0` to disable. |
113
+ | `AGENDEX_WATCHER_REFRESH_INTERVAL_MS` | `300000` | Re-discover watch directories (new Cursor projects, `@plans` folders, custom dirs). Set to `0` to disable. |
114
+
115
+ Typical latency after a local edit: **~0.5–2s** for file-based agents (Cursor, Claude Code, markdown snapshots); **~2–3s** for Plannotator live-session-only edits (loopback poll).
116
+
73
117
  ## Daemon Cleanup
74
118
 
75
119
  `agendex cleanup` manages registered daemon devices in the cloud.
package/dist/cli.js CHANGED
@@ -1060,7 +1060,7 @@ var init_cleanup = __esm(() => {
1060
1060
  // src/cli.ts
1061
1061
  import { spawn as spawn4 } from "node:child_process";
1062
1062
  import { existsSync as existsSync13, statSync as statSync2, writeSync } from "node:fs";
1063
- import { resolve as resolve10 } from "node:path";
1063
+ import { resolve as resolve11 } from "node:path";
1064
1064
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1065
1065
 
1066
1066
  // ../shared/src/adapters/catalog.ts
@@ -2958,6 +2958,7 @@ function isIndexablePlan(plan) {
2958
2958
  }
2959
2959
  var PROPOSED_PLAN_TAG_REGEX2 = /<\s*\/?\s*proposed_plan\s*>/gi;
2960
2960
  var ESCAPED_PROPOSED_PLAN_TAG_REGEX2 = /&lt;\s*\/?\s*proposed_plan\s*&gt;/gi;
2961
+ var FENCED_CODE_BLOCK_REGEX = /(?:```|~~~)[\s\S]*?(?:```|~~~)/g;
2961
2962
  var VISIBLE_TEXT_REGEX = /[\p{L}\p{N}]/u;
2962
2963
  var LOW_VALUE_METADATA_KEYS = ["lowValue", "lowValueReasons", "lowValueSignals"];
2963
2964
  function normalizeLineEndings2(text) {
@@ -2977,7 +2978,7 @@ function normalizePlanContent(content) {
2977
2978
  return stripBoundaryHtmlComments(normalizeLineEndings2(content).replace(/^\uFEFF/, "").replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "").replace(ESCAPED_PROPOSED_PLAN_TAG_REGEX2, "").replace(PROPOSED_PLAN_TAG_REGEX2, "")).trim();
2978
2979
  }
2979
2980
  function withoutLowValueMetadata(metadata) {
2980
- const next = { ...metadata ?? {} };
2981
+ const next = { ...metadata };
2981
2982
  for (const key of LOW_VALUE_METADATA_KEYS)
2982
2983
  delete next[key];
2983
2984
  return next;
@@ -2988,6 +2989,23 @@ function unique(items) {
2988
2989
  function visibleText(text) {
2989
2990
  return text.replace(/<!--[\s\S]*?-->/g, "").replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "").replace(ESCAPED_PROPOSED_PLAN_TAG_REGEX2, "").replace(PROPOSED_PLAN_TAG_REGEX2, "").replace(/[`*_#[\](){}<>:|~\-+=.]/g, " ").trim();
2990
2991
  }
2992
+ function stripFencedCodeBlocks(text) {
2993
+ return text.replace(FENCED_CODE_BLOCK_REGEX, " ");
2994
+ }
2995
+ function wordCount(text) {
2996
+ return text.match(/[\p{L}\p{N}_]+/gu)?.length ?? 0;
2997
+ }
2998
+ function codeBlockMetrics(text) {
2999
+ const blocks = text.match(FENCED_CODE_BLOCK_REGEX) ?? [];
3000
+ const codeCharCount = blocks.reduce((sum, block) => sum + block.length, 0);
3001
+ const nonCode = stripFencedCodeBlocks(text);
3002
+ return {
3003
+ codeBlockCount: blocks.length,
3004
+ codeCharCount,
3005
+ codeShare: codeCharCount / Math.max(text.length, 1),
3006
+ nonCodeWordCount: wordCount(visibleText(nonCode))
3007
+ };
3008
+ }
2991
3009
  function isSeparatorLine(line) {
2992
3010
  return /^(?:-{3,}|_{3,}|\*{3,})$/.test(line.trim());
2993
3011
  }
@@ -3011,7 +3029,7 @@ function isOrderedListLine(line) {
3011
3029
  return /^\d+[.)]\s+\S/.test(line.trim());
3012
3030
  }
3013
3031
  function isHeadingOnly(lines) {
3014
- return lines.length > 0 && lines.some(isHeadingLine) && lines.every(isHeadingLine);
3032
+ return lines.some(isHeadingLine) && lines.every(isHeadingLine);
3015
3033
  }
3016
3034
  function metadataHasPlanBlocks(metadata) {
3017
3035
  const planBlocks = metadata?.planBlocks;
@@ -3112,13 +3130,20 @@ function collectPositiveSignals(normalized, lines, metadata) {
3112
3130
  }).length;
3113
3131
  if (actionBulletCount >= 2)
3114
3132
  signals.push("action-bullets");
3115
- if (lines.length >= 2 && /\b(?:will|should|need to|needs to|plan to|planned|approach is to|implementation will)\b/i.test(normalized)) {
3133
+ const nonCodeVisible = visibleText(stripFencedCodeBlocks(normalized));
3134
+ if (lines.length >= 2 && /\b(?:will|should|need to|needs to|plan to|planned|approach is to|implementation will)\b/i.test(nonCodeVisible)) {
3116
3135
  signals.push("future-plan-language");
3117
3136
  }
3137
+ const planningPhraseCount = nonCodeVisible.match(/\b(?:will|should|need to|needs to|plan to|approach|implementation|implement|add|update|modify|create|remove|refactor|test|verify|validate|steps?|tasks?)\b/gi)?.length ?? 0;
3138
+ const proseWordCount = wordCount(nonCodeVisible);
3139
+ const isPlanningProse = proseWordCount >= 35 && planningPhraseCount >= 4 || proseWordCount >= 3 && planningPhraseCount >= 3 && planningPhraseCount / proseWordCount >= 0.4;
3140
+ if (isPlanningProse) {
3141
+ signals.push("planning-prose");
3142
+ }
3118
3143
  return unique(signals);
3119
3144
  }
3120
3145
  function isStrongPositiveSignal(signal) {
3121
- return signal === "metadata:proposed-plan-block" || signal === "checklist" || signal === "ordered-steps" || signal === "action-bullets" || signal === "section:approach" || signal === "section:implementation-plan" || signal === "section:files-to-modify" || signal === "section:steps" || signal === "section:acceptance-criteria";
3146
+ return signal === "metadata:proposed-plan-block" || signal === "checklist" || signal === "ordered-steps" || signal === "action-bullets" || signal === "planning-prose" || signal === "section:approach" || signal === "section:implementation-plan" || signal === "section:files-to-modify" || signal === "section:steps" || signal === "section:acceptance-criteria";
3122
3147
  }
3123
3148
  function hasStrongPlanSignal(positiveSignals) {
3124
3149
  const sectionCount = positiveSignals.filter((signal) => signal.startsWith("section:")).length;
@@ -3181,11 +3206,18 @@ function looksLikeSystemContext(normalized, lines) {
3181
3206
  function looksLikeToolLog(normalized) {
3182
3207
  return /::[a-z0-9_-]+(?:\{|\[|\s*$)/i.test(normalized) || /<\/?(?:tool_call|tool_result)\b[^>]*>/i.test(normalized) || /\b(?:function_call|tool_calls|tool_result)\b/i.test(normalized);
3183
3208
  }
3209
+ function looksLikeConversationArtifact(lines) {
3210
+ const roleLineCount = lines.filter((line) => /^(?:[-*+]\s+)?(?:\*\*)?(?:user|assistant|system|developer|tool|agent)(?:\*\*)?\s*:/i.test(line.trim())).length;
3211
+ if (roleLineCount >= 2)
3212
+ return true;
3213
+ const wrapperCount = lines.filter((line) => /^<\/?(?:user_action|user_prompt|assistant_response|message|conversation)\b/i.test(line.trim())).length;
3214
+ return wrapperCount >= 2;
3215
+ }
3184
3216
  function looksLikeExecutionReport(normalized) {
3185
3217
  const hasPastCompletion = /\b(?:fixed|pushed|committed|completed|done|implemented|updated|changed|patched|merged|deployed|passed|failed|resolved|reverted)\b/i.test(normalized);
3186
3218
  const hasReportSection = /^\s*(?:summary|result|results|changes|verification|status)\s*:/im.test(normalized);
3187
3219
  const hasReviewReportMarker = /\b(?:review findings?|review issues?|review comments?)\b/i.test(normalized);
3188
- 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);
3220
+ const hasCommandMarker = /::[a-z0-9_-]+(?:\{|\[|\s*$)/im.test(normalized) || /`[^`]*\b(?:bun|npm|pnpm|yarn|git|tsc|oxfmt|oxlint|biome)\b[^`]*`/i.test(normalized) || /\b(?:git\s+(?:stage|commit|push|status)|bunx?\s+|npm\s+|pnpm\s+|yarn\s+)\b/i.test(normalized);
3189
3221
  return hasPastCompletion && (hasReportSection || hasCommandMarker || hasReviewReportMarker);
3190
3222
  }
3191
3223
  function lowValueAssessment(reasons, signals) {
@@ -3212,14 +3244,20 @@ function assessPlanValue(input) {
3212
3244
  const strongPositive = hasStrongPlanSignal(positiveSignals);
3213
3245
  const systemContext = looksLikeSystemContext(normalized, lines);
3214
3246
  const toolLog = looksLikeToolLog(normalized);
3247
+ const conversationArtifact = looksLikeConversationArtifact(lines);
3215
3248
  const executionReport = looksLikeExecutionReport(normalized);
3216
3249
  const wrapperTitle = looksLikeWrapperTitle(input.title);
3217
3250
  const promptTitle = looksLikePromptTitle(input.title);
3218
3251
  const reviewOutput = looksLikeReviewOutput(normalized, input.title);
3252
+ const codeMetrics = codeBlockMetrics(normalized);
3253
+ const codeOnly = codeMetrics.codeBlockCount > 0 && codeMetrics.nonCodeWordCount === 0;
3254
+ const codeDominated = codeMetrics.codeBlockCount > 0 && codeMetrics.codeShare >= 0.6 && codeMetrics.nonCodeWordCount < 120;
3219
3255
  if (systemContext)
3220
3256
  signals.push("negative:system-context");
3221
3257
  if (toolLog)
3222
3258
  signals.push("negative:tool-log");
3259
+ if (conversationArtifact)
3260
+ signals.push("negative:conversation-artifact");
3223
3261
  if (executionReport)
3224
3262
  signals.push("negative:execution-report");
3225
3263
  if (wrapperTitle)
@@ -3228,24 +3266,36 @@ function assessPlanValue(input) {
3228
3266
  signals.push("negative:prompt-title");
3229
3267
  if (reviewOutput)
3230
3268
  signals.push("negative:review-output");
3269
+ if (codeMetrics.codeBlockCount > 0)
3270
+ signals.push("shape:code-blocks");
3271
+ if (codeOnly)
3272
+ signals.push("negative:code-only");
3273
+ if (codeDominated)
3274
+ signals.push("negative:code-dominated");
3231
3275
  if (lines.length === 1)
3232
3276
  signals.push("shape:single-line");
3233
3277
  if (lines.length === 1 && positiveSignals.length === 0) {
3234
3278
  reasons.push(isPromptLikeOneLiner(lines[0] ?? "") ? "prompt-like" : "no-plan-signals");
3235
3279
  }
3236
- if (systemContext && !strongPositive)
3280
+ if (systemContext && !explicitPlanBlock)
3237
3281
  reasons.push("system-context");
3238
- if (executionReport && !strongPositive)
3282
+ if (toolLog && !explicitPlanBlock)
3283
+ reasons.push("tool-log");
3284
+ if (conversationArtifact && !explicitPlanBlock && !strongPositive)
3285
+ reasons.push("conversation-artifact");
3286
+ if (executionReport && !explicitPlanBlock && !strongPositive)
3239
3287
  reasons.push("execution-report");
3240
3288
  if (wrapperTitle && !explicitPlanBlock)
3241
3289
  reasons.push("wrapper-title");
3242
3290
  if (reviewOutput && !explicitPlanBlock)
3243
3291
  reasons.push("review-output");
3244
- if (promptTitle && !strongPositive && positiveSignals.length === 0)
3292
+ if (promptTitle && !strongPositive && !explicitPlanBlock)
3245
3293
  reasons.push("prompt-like");
3246
- if (toolLog && !strongPositive && positiveSignals.length === 0)
3247
- reasons.push("no-plan-signals");
3248
- if (reasons.length === 0 && positiveSignals.length === 0 && lines.length <= 3) {
3294
+ if (codeOnly && !explicitPlanBlock && !strongPositive)
3295
+ reasons.push("code-only");
3296
+ if (codeDominated && !explicitPlanBlock && !strongPositive)
3297
+ reasons.push("code-dominated");
3298
+ if (reasons.length === 0 && !strongPositive && !positiveSignals.includes("planning-prose")) {
3249
3299
  reasons.push("no-plan-signals");
3250
3300
  }
3251
3301
  return lowValueAssessment(reasons, signals);
@@ -3426,6 +3476,9 @@ async function walkDir(dir, depth = 0, seen = new Set) {
3426
3476
  } catch {}
3427
3477
  return files;
3428
3478
  }
3479
+ function preparePlanForIndex(plan) {
3480
+ return annotatePlanValueMetadata(plan);
3481
+ }
3429
3482
  async function parseGenericMarkdownPlan(filePath, extraMetadata) {
3430
3483
  try {
3431
3484
  const content = await readFile7(filePath, "utf-8");
@@ -3465,7 +3518,7 @@ async function scanUserPlans(into) {
3465
3518
  continue;
3466
3519
  const plan = await parseGenericMarkdownPlan(file, { userCreated: true });
3467
3520
  if (plan)
3468
- into.set(plan.id, plan);
3521
+ into.set(plan.id, preparePlanForIndex(plan));
3469
3522
  }
3470
3523
  }
3471
3524
  function getCustomPlanDirs() {
@@ -3527,7 +3580,7 @@ async function scanCustomPlanDirs(coveredPaths, into) {
3527
3580
  agentHint: dirBasename
3528
3581
  });
3529
3582
  if (plan) {
3530
- into.set(plan.id, plan);
3583
+ into.set(plan.id, preparePlanForIndex(plan));
3531
3584
  count++;
3532
3585
  }
3533
3586
  }
@@ -3547,7 +3600,7 @@ async function scan() {
3547
3600
  continue;
3548
3601
  const plans = await adapter.parse(file);
3549
3602
  for (const plan of plans) {
3550
- const annotated = annotatePlanValueMetadata(plan);
3603
+ const annotated = preparePlanForIndex(plan);
3551
3604
  next.set(annotated.id, annotated);
3552
3605
  }
3553
3606
  }
@@ -3567,7 +3620,7 @@ async function scan() {
3567
3620
  continue;
3568
3621
  const plans = await adapter.parse(file);
3569
3622
  for (const plan of plans) {
3570
- const annotated = annotatePlanValueMetadata(plan);
3623
+ const annotated = preparePlanForIndex(plan);
3571
3624
  next.set(annotated.id, annotated);
3572
3625
  }
3573
3626
  }
@@ -3664,7 +3717,7 @@ async function rescanFile(filePath) {
3664
3717
  removedPlans.push(...removePlansForPath(filePath, adapter));
3665
3718
  continue;
3666
3719
  }
3667
- const plans = rawPlans.map(annotatePlanValueMetadata);
3720
+ const plans = rawPlans.map(preparePlanForIndex);
3668
3721
  for (const plan of plans) {
3669
3722
  store.set(plan.id, plan);
3670
3723
  }
@@ -3677,9 +3730,10 @@ async function rescanFile(filePath) {
3677
3730
  if (normalized.endsWith(".md") && (normalized.startsWith(userPlansDir + sep3) || normalized === userPlansDir)) {
3678
3731
  const plan = await parseGenericMarkdownPlan(filePath, { userCreated: true });
3679
3732
  if (plan) {
3680
- store.set(plan.id, plan);
3733
+ const annotated = preparePlanForIndex(plan);
3734
+ store.set(annotated.id, annotated);
3681
3735
  notifyPlansChanged();
3682
- return [plan];
3736
+ return [annotated];
3683
3737
  }
3684
3738
  }
3685
3739
  if (normalized.endsWith(".md")) {
@@ -3692,9 +3746,10 @@ async function rescanFile(filePath) {
3692
3746
  customDir: dir
3693
3747
  });
3694
3748
  if (plan) {
3695
- store.set(plan.id, plan);
3749
+ const annotated = preparePlanForIndex(plan);
3750
+ store.set(annotated.id, annotated);
3696
3751
  notifyPlansChanged();
3697
- return [plan];
3752
+ return [annotated];
3698
3753
  }
3699
3754
  }
3700
3755
  }
@@ -3755,6 +3810,38 @@ function startWatching(onChange) {
3755
3810
  function stopWatching() {
3756
3811
  closeAllWatchers();
3757
3812
  }
3813
+ function collectWatchPaths() {
3814
+ const adapters = getActiveAdapters();
3815
+ const watchedPaths = new Set;
3816
+ for (const adapter of adapters) {
3817
+ for (const watchPath of adapter.getWatchPaths()) {
3818
+ watchedPaths.add(resolve5(watchPath));
3819
+ }
3820
+ }
3821
+ const discovered = discoverProjectPlanDirs();
3822
+ for (const { dir, agent } of discovered) {
3823
+ if (watchedPaths.has(resolve5(dir)))
3824
+ continue;
3825
+ const adapter = adapters.find((a) => a.agent === agent);
3826
+ if (!adapter)
3827
+ continue;
3828
+ watchedPaths.add(resolve5(dir));
3829
+ }
3830
+ for (const dir of getCustomPlanDirs()) {
3831
+ const resolvedCustom = resolve5(dir);
3832
+ let overlaps = false;
3833
+ for (const watched of watchedPaths) {
3834
+ if (pathsOverlapFilesystemTree(resolvedCustom, watched)) {
3835
+ overlaps = true;
3836
+ break;
3837
+ }
3838
+ }
3839
+ if (overlaps)
3840
+ continue;
3841
+ watchedPaths.add(resolvedCustom);
3842
+ }
3843
+ return [...watchedPaths].sort();
3844
+ }
3758
3845
  function setupWatchers(onChange) {
3759
3846
  const adapters = getActiveAdapters();
3760
3847
  const watchedPaths = new Set;
@@ -4418,6 +4505,86 @@ function resolveCliAdapterIds(config) {
4418
4505
  return ids;
4419
4506
  }
4420
4507
 
4508
+ // src/sync-cache.ts
4509
+ import { createHash as createHash2 } from "node:crypto";
4510
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4511
+ import { join as join12 } from "node:path";
4512
+ function getCachePath() {
4513
+ return join12(getConfigDir(), "sync-cache.json");
4514
+ }
4515
+ function loadSyncCache() {
4516
+ const cachePath = getCachePath();
4517
+ if (!existsSync8(cachePath))
4518
+ return {};
4519
+ try {
4520
+ const raw = JSON.parse(readFileSync5(cachePath, "utf-8"));
4521
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
4522
+ return {};
4523
+ return raw;
4524
+ } catch {
4525
+ return {};
4526
+ }
4527
+ }
4528
+ function saveSyncCache(cache, options) {
4529
+ const dir = getConfigDir();
4530
+ if (!existsSync8(dir))
4531
+ mkdirSync3(dir, { recursive: true });
4532
+ const cachePath = getCachePath();
4533
+ if (options?.replace) {
4534
+ writeFileSync3(cachePath, JSON.stringify(cache));
4535
+ return;
4536
+ }
4537
+ const existing = loadSyncCache();
4538
+ writeFileSync3(cachePath, JSON.stringify({ ...existing, ...cache }));
4539
+ }
4540
+ function computePayloadHash(payload) {
4541
+ const canonical = JSON.stringify([
4542
+ payload.localPlanId,
4543
+ payload.agent,
4544
+ payload.title,
4545
+ payload.content,
4546
+ payload.format,
4547
+ payload.filePath ?? null,
4548
+ payload.workspace ?? null,
4549
+ payload.metadata ?? null,
4550
+ payload.createdAt ?? null,
4551
+ payload.updatedAt ?? null
4552
+ ]);
4553
+ return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
4554
+ }
4555
+
4556
+ // src/daemon-sync.ts
4557
+ var DEFAULT_LIVE_SESSION_POLL_MS = 2000;
4558
+ var DEFAULT_SYNC_RESCAN_INTERVAL_MS = 60000;
4559
+ var DEFAULT_WATCHER_REFRESH_INTERVAL_MS = 300000;
4560
+ var SYNC_RETRY_DELAYS_MS = [2000, 8000, 30000];
4561
+ var SYNC_MAX_RETRIES = SYNC_RETRY_DELAYS_MS.length;
4562
+ function parseEnvMs(name, defaultMs) {
4563
+ const raw = process.env[name];
4564
+ if (raw === undefined || raw === "")
4565
+ return defaultMs;
4566
+ const parsed = Number.parseInt(raw, 10);
4567
+ if (!Number.isFinite(parsed) || parsed < 0)
4568
+ return defaultMs;
4569
+ return parsed;
4570
+ }
4571
+ function dedupeSyncPayloads(payloads) {
4572
+ const byId = new Map;
4573
+ for (const payload of payloads) {
4574
+ byId.set(payload.localPlanId, payload);
4575
+ }
4576
+ return [...byId.values()];
4577
+ }
4578
+ function payloadNeedsSync(payload, cache) {
4579
+ return cache[payload.localPlanId] !== computePayloadHash(payload);
4580
+ }
4581
+ function filterPayloadsNeedingSync(payloads, cache) {
4582
+ return payloads.filter((payload) => payloadNeedsSync(payload, cache));
4583
+ }
4584
+ function nextRetryDelayMs(attempt) {
4585
+ return SYNC_RETRY_DELAYS_MS[attempt];
4586
+ }
4587
+
4421
4588
  // src/network.ts
4422
4589
  import { execFileSync } from "node:child_process";
4423
4590
  import { networkInterfaces } from "node:os";
@@ -4598,54 +4765,6 @@ function planToSyncPayload(plan, deviceId, hostname2, ipAddress) {
4598
4765
  };
4599
4766
  }
4600
4767
 
4601
- // src/sync-cache.ts
4602
- import { createHash as createHash2 } from "node:crypto";
4603
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4604
- import { join as join12 } from "node:path";
4605
- function getCachePath() {
4606
- return join12(getConfigDir(), "sync-cache.json");
4607
- }
4608
- function loadSyncCache() {
4609
- const cachePath = getCachePath();
4610
- if (!existsSync8(cachePath))
4611
- return {};
4612
- try {
4613
- const raw = JSON.parse(readFileSync5(cachePath, "utf-8"));
4614
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
4615
- return {};
4616
- return raw;
4617
- } catch {
4618
- return {};
4619
- }
4620
- }
4621
- function saveSyncCache(cache, options) {
4622
- const dir = getConfigDir();
4623
- if (!existsSync8(dir))
4624
- mkdirSync3(dir, { recursive: true });
4625
- const cachePath = getCachePath();
4626
- if (options?.replace) {
4627
- writeFileSync3(cachePath, JSON.stringify(cache));
4628
- return;
4629
- }
4630
- const existing = loadSyncCache();
4631
- writeFileSync3(cachePath, JSON.stringify({ ...existing, ...cache }));
4632
- }
4633
- function computePayloadHash(payload) {
4634
- const canonical = JSON.stringify([
4635
- payload.localPlanId,
4636
- payload.agent,
4637
- payload.title,
4638
- payload.content,
4639
- payload.format,
4640
- payload.filePath ?? null,
4641
- payload.workspace ?? null,
4642
- payload.metadata ?? null,
4643
- payload.createdAt ?? null,
4644
- payload.updatedAt ?? null
4645
- ]);
4646
- return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
4647
- }
4648
-
4649
4768
  // src/sync-privacy.ts
4650
4769
  async function shouldIncludeLocalIpAddressInSync() {
4651
4770
  if (!shouldCollectLocalIpAddress())
@@ -4712,6 +4831,10 @@ var PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS = 15000;
4712
4831
  var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4713
4832
  var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the write-back payload.";
4714
4833
  var PLANNOTATOR_LIVENESS_SWEEP_INTERVAL_MS = 20000;
4834
+ var LIVE_SESSION_POLL_MS = parseEnvMs("AGENDEX_LIVE_SESSION_POLL_MS", DEFAULT_LIVE_SESSION_POLL_MS);
4835
+ var SYNC_RESCAN_INTERVAL_MS = parseEnvMs("AGENDEX_SYNC_RESCAN_INTERVAL_MS", DEFAULT_SYNC_RESCAN_INTERVAL_MS);
4836
+ var WATCHER_REFRESH_INTERVAL_MS = parseEnvMs("AGENDEX_WATCHER_REFRESH_INTERVAL_MS", DEFAULT_WATCHER_REFRESH_INTERVAL_MS);
4837
+ var RETRY_TICK_INTERVAL_MS = 1000;
4715
4838
  function isRecord5(value) {
4716
4839
  return typeof value === "object" && value !== null;
4717
4840
  }
@@ -4745,6 +4868,9 @@ async function runWorker() {
4745
4868
  setActiveAdapters(adapters);
4746
4869
  const syncCache = loadSyncCache();
4747
4870
  const syncQueue = [];
4871
+ const retryQueue = [];
4872
+ const retryAttemptByPlanId = new Map;
4873
+ const lastSyncedUpdatedAt = new Map;
4748
4874
  const pendingWritebackReports = loadPendingWritebackReports();
4749
4875
  const liveSessions = new Map;
4750
4876
  let syncing = false;
@@ -4770,17 +4896,76 @@ async function runWorker() {
4770
4896
  console.log("[agendex] cloud token refreshed");
4771
4897
  return true;
4772
4898
  }
4899
+ function pushToSyncQueue(...payloads) {
4900
+ for (const payload of payloads) {
4901
+ const idx = syncQueue.findIndex((p) => p.localPlanId === payload.localPlanId);
4902
+ if (idx >= 0) {
4903
+ const existing = syncQueue[idx];
4904
+ if (existing?.updatedAt !== undefined && payload.updatedAt !== undefined && existing.updatedAt >= payload.updatedAt) {
4905
+ continue;
4906
+ }
4907
+ syncQueue[idx] = payload;
4908
+ } else {
4909
+ syncQueue.push(payload);
4910
+ }
4911
+ }
4912
+ }
4913
+ function scheduleSyncRetry(payload, attempt) {
4914
+ const delayMs = nextRetryDelayMs(attempt);
4915
+ if (delayMs === undefined) {
4916
+ console.error(`[agendex] sync gave up for "${payload.title}" after ${SYNC_MAX_RETRIES} attempts`);
4917
+ return;
4918
+ }
4919
+ retryQueue.push({ payload, attempt: attempt + 1, retryAt: Date.now() + delayMs });
4920
+ }
4921
+ function flushReadyRetries() {
4922
+ const now = Date.now();
4923
+ let moved = 0;
4924
+ for (let i = retryQueue.length - 1;i >= 0; i--) {
4925
+ const entry = retryQueue[i];
4926
+ if (!entry || entry.retryAt > now)
4927
+ continue;
4928
+ retryQueue.splice(i, 1);
4929
+ const lastSynced = lastSyncedUpdatedAt.get(entry.payload.localPlanId);
4930
+ if (lastSynced !== undefined && entry.payload.updatedAt !== undefined && lastSynced >= entry.payload.updatedAt) {
4931
+ continue;
4932
+ }
4933
+ pushToSyncQueue(entry.payload);
4934
+ retryAttemptByPlanId.set(entry.payload.localPlanId, entry.attempt);
4935
+ moved++;
4936
+ }
4937
+ if (moved > 0)
4938
+ processSyncQueue();
4939
+ }
4940
+ async function enqueueChangedPlans(plans2) {
4941
+ if (plans2.length === 0)
4942
+ return 0;
4943
+ const ipAddress = await getSyncIpAddress();
4944
+ const payloads = plans2.map((plan) => planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4945
+ const needingSync = filterPayloadsNeedingSync(payloads, syncCache);
4946
+ if (needingSync.length === 0)
4947
+ return 0;
4948
+ pushToSyncQueue(...needingSync);
4949
+ processSyncQueue();
4950
+ return needingSync.length;
4951
+ }
4773
4952
  async function processSyncQueue() {
4774
4953
  if (syncing || syncQueue.length === 0)
4775
4954
  return;
4776
4955
  syncing = true;
4777
- const batch = syncQueue.splice(0);
4956
+ const batch = dedupeSyncPayloads(syncQueue.splice(0));
4778
4957
  let syncedCount = 0;
4779
4958
  let lowValueSkippedCount = 0;
4780
4959
  let lowValueDeletedCount = 0;
4781
4960
  let failedCount = 0;
4961
+ const failedPayloads = [];
4782
4962
  try {
4783
4963
  for (const payload of batch) {
4964
+ const lastSynced = lastSyncedUpdatedAt.get(payload.localPlanId);
4965
+ if (lastSynced !== undefined && payload.updatedAt !== undefined && lastSynced >= payload.updatedAt) {
4966
+ retryAttemptByPlanId.delete(payload.localPlanId);
4967
+ continue;
4968
+ }
4784
4969
  let result = await syncPlan(payload);
4785
4970
  if (!result.ok && result.error?.includes("401")) {
4786
4971
  const refreshed = await tryRefreshToken();
@@ -4791,11 +4976,12 @@ async function runWorker() {
4791
4976
  if (!result.ok) {
4792
4977
  if (result.error?.includes("401")) {
4793
4978
  console.error("[agendex] session expired. Run `agendex login` to re-authenticate.");
4794
- batch.length = 0;
4795
4979
  syncQueue.length = 0;
4980
+ retryQueue.length = 0;
4796
4981
  break;
4797
4982
  }
4798
4983
  failedCount++;
4984
+ failedPayloads.push(payload);
4799
4985
  console.error(`[agendex] sync failed for "${payload.title}": ${result.error}`);
4800
4986
  } else {
4801
4987
  if (result.skippedLowValue) {
@@ -4806,14 +4992,23 @@ async function runWorker() {
4806
4992
  syncedCount++;
4807
4993
  }
4808
4994
  syncCache[payload.localPlanId] = computePayloadHash(payload);
4995
+ retryAttemptByPlanId.delete(payload.localPlanId);
4996
+ if (payload.updatedAt !== undefined) {
4997
+ lastSyncedUpdatedAt.set(payload.localPlanId, payload.updatedAt);
4998
+ }
4809
4999
  }
4810
5000
  }
4811
5001
  } catch (err) {
4812
5002
  console.error("[agendex] sync error:", err);
4813
- syncQueue.unshift(...batch.slice(syncedCount + lowValueSkippedCount + failedCount));
5003
+ pushToSyncQueue(...batch.slice(syncedCount + lowValueSkippedCount + failedCount));
4814
5004
  } finally {
4815
5005
  syncing = false;
4816
5006
  }
5007
+ for (const payload of failedPayloads) {
5008
+ const attempt = retryAttemptByPlanId.get(payload.localPlanId) ?? 0;
5009
+ retryAttemptByPlanId.delete(payload.localPlanId);
5010
+ scheduleSyncRetry(payload, attempt);
5011
+ }
4817
5012
  if (syncedCount > 0 || lowValueSkippedCount > 0 || failedCount > 0) {
4818
5013
  saveSyncCache(syncCache);
4819
5014
  const lowValueSuffix2 = lowValueSkippedCount > 0 ? `, ${lowValueSkippedCount} low-value skipped/pruned${lowValueDeletedCount > 0 ? ` (${lowValueDeletedCount} deleted)` : ""}` : "";
@@ -4835,7 +5030,7 @@ async function runWorker() {
4835
5030
  if (livePayloads.has(planId))
4836
5031
  continue;
4837
5032
  const endedPayload = buildEndedPlannotatorPayload(lastPayload);
4838
- syncQueue.push(endedPayload);
5033
+ pushToSyncQueue(endedPayload);
4839
5034
  liveSessions.delete(planId);
4840
5035
  queued = true;
4841
5036
  console.log(`[agendex] Plannotator session ended: ${endedPayload.title}`);
@@ -4901,7 +5096,7 @@ async function runWorker() {
4901
5096
  const updatedPlan = getById(job.localPlanId);
4902
5097
  if (updatedPlan) {
4903
5098
  const updatedPayload = planToSyncPayload(updatedPlan, config.deviceId, hostname2, await getSyncIpAddress());
4904
- syncQueue.push(updatedPayload);
5099
+ pushToSyncQueue(updatedPayload);
4905
5100
  if (isLivePlannotatorPayload(updatedPayload)) {
4906
5101
  liveSessions.set(updatedPayload.localPlanId, updatedPayload);
4907
5102
  }
@@ -4936,7 +5131,15 @@ async function runWorker() {
4936
5131
  pollingWritebacks = false;
4937
5132
  }
4938
5133
  }
4939
- setOnPlansChanged(() => {});
5134
+ let lastWatchPathKey = collectWatchPaths().join("\x00");
5135
+ const onPlansFileChange = (changedPlans) => {
5136
+ (async () => {
5137
+ await enqueueChangedPlans(changedPlans);
5138
+ await reconcileLivePlannotatorSessions(getAll());
5139
+ })().catch((err) => {
5140
+ console.error("[agendex] failed to queue changed plans:", err);
5141
+ });
5142
+ };
4940
5143
  console.log(`[agendex] initial scan...`);
4941
5144
  await scan();
4942
5145
  const plans = getAll();
@@ -4948,8 +5151,7 @@ async function runWorker() {
4948
5151
  const initialIpAddress = await getSyncIpAddress();
4949
5152
  for (const plan of plans) {
4950
5153
  const payload = planToSyncPayload(plan, config.deviceId, hostname2, initialIpAddress);
4951
- const hash = computePayloadHash(payload);
4952
- if (syncCache[plan.id] === hash) {
5154
+ if (syncCache[plan.id] === computePayloadHash(payload)) {
4953
5155
  initialSkipped++;
4954
5156
  continue;
4955
5157
  }
@@ -4958,7 +5160,7 @@ async function runWorker() {
4958
5160
  } else {
4959
5161
  initialQueuedSyncable++;
4960
5162
  }
4961
- syncQueue.push(payload);
5163
+ pushToSyncQueue(payload);
4962
5164
  }
4963
5165
  const activePlanIds = new Set(plans.map((plan) => plan.id));
4964
5166
  for (const id of Object.keys(syncCache)) {
@@ -4970,35 +5172,75 @@ async function runWorker() {
4970
5172
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
4971
5173
  await processSyncQueue();
4972
5174
  await reconcileLivePlannotatorSessions(getAll());
5175
+ setOnPlansChanged((plans2) => {
5176
+ enqueueChangedPlans(plans2).catch((err) => {
5177
+ console.error("[agendex] failed to sync plan store changes:", err);
5178
+ });
5179
+ });
4973
5180
  setInterval(() => {
4974
5181
  (async () => {
4975
5182
  await sendHeartbeat(await getSyncIpAddress());
4976
5183
  })().catch(() => {});
4977
5184
  }, CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
5185
+ setInterval(() => {
5186
+ flushReadyRetries();
5187
+ }, RETRY_TICK_INTERVAL_MS);
5188
+ if (SYNC_RESCAN_INTERVAL_MS > 0) {
5189
+ setInterval(() => {
5190
+ (async () => {
5191
+ await scan();
5192
+ await enqueueChangedPlans(getAll());
5193
+ await reconcileLivePlannotatorSessions(getAll());
5194
+ })().catch((err) => {
5195
+ console.error("[agendex] safety rescan failed:", err);
5196
+ });
5197
+ }, SYNC_RESCAN_INTERVAL_MS);
5198
+ }
5199
+ if (WATCHER_REFRESH_INTERVAL_MS > 0) {
5200
+ setInterval(() => {
5201
+ const nextKey = collectWatchPaths().join("\x00");
5202
+ if (nextKey === lastWatchPathKey)
5203
+ return;
5204
+ lastWatchPathKey = nextKey;
5205
+ console.log("[agendex] watch paths changed, refreshing file watchers...");
5206
+ startWatching(onPlansFileChange);
5207
+ }, WATCHER_REFRESH_INTERVAL_MS);
5208
+ }
4978
5209
  if (shouldEnablePlannotatorSync(config)) {
4979
5210
  setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4980
5211
  pollPlannotatorWritebacks();
4981
5212
  setInterval(() => {
4982
5213
  (async () => {
4983
5214
  await scan();
5215
+ await enqueueChangedPlans(getAll());
4984
5216
  await reconcileLivePlannotatorSessions(getAll());
4985
5217
  })().catch((err) => {
4986
5218
  console.error("[agendex] Plannotator liveness sweep failed:", err);
4987
5219
  });
4988
5220
  }, PLANNOTATOR_LIVENESS_SWEEP_INTERVAL_MS);
5221
+ if (LIVE_SESSION_POLL_MS > 0) {
5222
+ setInterval(() => {
5223
+ (async () => {
5224
+ const ipAddress = await getSyncIpAddress();
5225
+ const livePlans = getAll().filter((plan) => {
5226
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, ipAddress);
5227
+ return isLivePlannotatorPayload(payload);
5228
+ });
5229
+ if (livePlans.length === 0)
5230
+ return;
5231
+ await scan();
5232
+ const refreshedLive = getAll().filter((plan) => {
5233
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, ipAddress);
5234
+ return isLivePlannotatorPayload(payload);
5235
+ });
5236
+ await enqueueChangedPlans(refreshedLive);
5237
+ })().catch((err) => {
5238
+ console.error("[agendex] live session poll failed:", err);
5239
+ });
5240
+ }, LIVE_SESSION_POLL_MS);
5241
+ }
4989
5242
  }
4990
- startWatching((changedPlans) => {
4991
- (async () => {
4992
- const ipAddress = await getSyncIpAddress();
4993
- for (const plan of changedPlans) {
4994
- syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4995
- }
4996
- processSyncQueue();
4997
- await reconcileLivePlannotatorSessions(getAll());
4998
- })().catch((err) => {
4999
- console.error("[agendex] failed to queue changed plans:", err);
5000
- });
5001
- });
5243
+ startWatching(onPlansFileChange);
5002
5244
  console.log(`[agendex] daemon running. Watching for file changes...`);
5003
5245
  async function gracefulShutdown() {
5004
5246
  stopWatching();
@@ -5502,7 +5744,7 @@ import { join as join15 } from "node:path";
5502
5744
  // package.json
5503
5745
  var package_default = {
5504
5746
  name: "agendex-cli",
5505
- version: "1.2.0",
5747
+ version: "1.4.0",
5506
5748
  description: "Agendex CLI for login, sync, and daemon workflows",
5507
5749
  homepage: "https://github.com/Tyru5/Agendex#readme",
5508
5750
  bugs: {
@@ -5813,6 +6055,7 @@ async function runUpgrade(opts) {
5813
6055
  import { readFile as readFile8, stat as stat8 } from "node:fs/promises";
5814
6056
  import { existsSync as existsSync12 } from "node:fs";
5815
6057
  import { hostname as osHostname4 } from "node:os";
6058
+ import { isAbsolute, resolve as resolve10 } from "node:path";
5816
6059
  function resolveAgentOverride(args) {
5817
6060
  const idx = args.indexOf("--agent");
5818
6061
  if (idx === -1)
@@ -5839,6 +6082,22 @@ function resolvePathArg(args) {
5839
6082
  }
5840
6083
  return;
5841
6084
  }
6085
+ function resolveLaunchCwd() {
6086
+ const initCwd = process.env.INIT_CWD?.trim();
6087
+ if (initCwd && isAbsolute(initCwd))
6088
+ return initCwd;
6089
+ const shellPwd = process.env.PWD?.trim();
6090
+ if (shellPwd && isAbsolute(shellPwd))
6091
+ return shellPwd;
6092
+ return process.cwd();
6093
+ }
6094
+ function resolveUploadFilePath(pathArg) {
6095
+ const trimmed = pathArg.trim();
6096
+ if (isAbsolute(trimmed) || trimmed === "~" || trimmed.startsWith("~/")) {
6097
+ return resolveCustomPlanDirPath(trimmed);
6098
+ }
6099
+ return resolve10(resolveLaunchCwd(), trimmed);
6100
+ }
5842
6101
  async function runUpload(args, deps) {
5843
6102
  const syncPlanFn = deps?.syncPlan ?? syncPlan;
5844
6103
  const log = deps?.log ?? ((m) => console.log(m));
@@ -5849,7 +6108,7 @@ async function runUpload(args, deps) {
5849
6108
  error("[agendex] usage: agendex upload <path> [--agent <name>] [--open]");
5850
6109
  return 1;
5851
6110
  }
5852
- const absolutePath = resolveCustomPlanDirPath(pathArg);
6111
+ const absolutePath = resolveUploadFilePath(pathArg);
5853
6112
  if (!existsSync12(absolutePath)) {
5854
6113
  error(`[agendex] path does not exist: ${absolutePath}`);
5855
6114
  return 1;
@@ -5923,7 +6182,7 @@ async function runUpload(args, deps) {
5923
6182
  // src/web.ts
5924
6183
  async function openAgendexWeb(siteUrlOverride) {
5925
6184
  const base = siteUrlOverride ?? getSiteUrl();
5926
- const url = base.replace(/\/$/, "");
6185
+ const url = `${base.replace(/\/$/, "")}/dashboard`;
5927
6186
  launchBrowser(url, "Agendex in your browser");
5928
6187
  }
5929
6188
  async function openSharedPlan(url) {
@@ -5957,7 +6216,7 @@ function firstCommandToken(argv) {
5957
6216
  return;
5958
6217
  }
5959
6218
  var command = firstCommandToken(args) ?? "start";
5960
- var cliEntry = resolve10(process.argv[1] ?? fileURLToPath3(import.meta.url));
6219
+ var cliEntry = resolve11(process.argv[1] ?? fileURLToPath3(import.meta.url));
5961
6220
  async function main() {
5962
6221
  const isInternal = args.includes("--daemon") || args.includes("--worker");
5963
6222
  if (command === "--version" || command === "-v") {
@@ -6188,7 +6447,7 @@ async function main() {
6188
6447
  const { request } = await import("node:http");
6189
6448
  const body = JSON.stringify({ path: resolved });
6190
6449
  try {
6191
- const res = await new Promise((resolve11, reject) => {
6450
+ const res = await new Promise((resolve12, reject) => {
6192
6451
  const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
6193
6452
  method: "POST",
6194
6453
  headers: {
@@ -6202,7 +6461,7 @@ async function main() {
6202
6461
  res2.on("data", (chunk) => {
6203
6462
  data += chunk;
6204
6463
  });
6205
- res2.on("end", () => resolve11({ status: res2.statusCode ?? 0, body: data }));
6464
+ res2.on("end", () => resolve12({ status: res2.statusCode ?? 0, body: data }));
6206
6465
  res2.on("error", reject);
6207
6466
  });
6208
6467
  req.on("error", reject);
@@ -6398,13 +6657,13 @@ function flushStream(stream) {
6398
6657
  if (stream.destroyed || !stream.writable) {
6399
6658
  return Promise.resolve();
6400
6659
  }
6401
- return new Promise((resolve11, reject) => {
6660
+ return new Promise((resolve12, reject) => {
6402
6661
  stream.write("", (error) => {
6403
6662
  if (error) {
6404
6663
  reject(error);
6405
6664
  return;
6406
6665
  }
6407
- resolve11();
6666
+ resolve12();
6408
6667
  });
6409
6668
  });
6410
6669
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {