agendex-cli 1.2.0 → 1.3.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.
- package/README.md +44 -0
- package/dist/cli.js +331 -89
- 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
|
@@ -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 = /<\s*\/?\s*proposed_plan\s*>/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.
|
|
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
|
-
|
|
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,6 +3206,13 @@ 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);
|
|
@@ -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 && !
|
|
3280
|
+
if (systemContext && !explicitPlanBlock)
|
|
3237
3281
|
reasons.push("system-context");
|
|
3238
|
-
if (
|
|
3282
|
+
if (toolLog && !explicitPlanBlock)
|
|
3283
|
+
reasons.push("tool-log");
|
|
3284
|
+
if (conversationArtifact && !explicitPlanBlock && !strongPositive)
|
|
3285
|
+
reasons.push("conversation-artifact");
|
|
3286
|
+
if (executionReport && !explicitPlanBlock)
|
|
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 &&
|
|
3292
|
+
if (promptTitle && !strongPositive && !explicitPlanBlock)
|
|
3245
3293
|
reasons.push("prompt-like");
|
|
3246
|
-
if (
|
|
3247
|
-
reasons.push("
|
|
3248
|
-
if (
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
3733
|
+
const annotated = preparePlanForIndex(plan);
|
|
3734
|
+
store.set(annotated.id, annotated);
|
|
3681
3735
|
notifyPlansChanged();
|
|
3682
|
-
return [
|
|
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
|
-
|
|
3749
|
+
const annotated = preparePlanForIndex(plan);
|
|
3750
|
+
store.set(annotated.id, annotated);
|
|
3696
3751
|
notifyPlansChanged();
|
|
3697
|
-
return [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
5747
|
+
version: "1.3.0",
|
|
5506
5748
|
description: "Agendex CLI for login, sync, and daemon workflows",
|
|
5507
5749
|
homepage: "https://github.com/Tyru5/Agendex#readme",
|
|
5508
5750
|
bugs: {
|