agendex-cli 0.12.0 → 0.14.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/dist/cli.js +489 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
4
5
|
var __defProp = Object.defineProperty;
|
|
@@ -44,6 +45,7 @@ var __export = (target, all) => {
|
|
|
44
45
|
});
|
|
45
46
|
};
|
|
46
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
48
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
47
49
|
|
|
48
50
|
// ../../node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
49
51
|
var require_picocolors = __commonJS((exports, module) => {
|
|
@@ -2254,6 +2256,16 @@ async function promptForAdapterSelection(options = {}) {
|
|
|
2254
2256
|
|
|
2255
2257
|
// ../shared/src/config.ts
|
|
2256
2258
|
var devModeOverride;
|
|
2259
|
+
function getHomeDir() {
|
|
2260
|
+
if (process.env.HOME)
|
|
2261
|
+
return process.env.HOME;
|
|
2262
|
+
if (process.env.USERPROFILE)
|
|
2263
|
+
return process.env.USERPROFILE;
|
|
2264
|
+
if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
|
|
2265
|
+
return `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`;
|
|
2266
|
+
}
|
|
2267
|
+
return homedir7();
|
|
2268
|
+
}
|
|
2257
2269
|
function setDevMode(dev) {
|
|
2258
2270
|
devModeOverride = dev;
|
|
2259
2271
|
}
|
|
@@ -2263,7 +2275,7 @@ function isDevMode() {
|
|
|
2263
2275
|
return process.env.AGENDEX_DEV === "1";
|
|
2264
2276
|
}
|
|
2265
2277
|
function getConfigDir() {
|
|
2266
|
-
return join7(
|
|
2278
|
+
return join7(getHomeDir(), isDevMode() ? ".agendex-dev" : ".agendex");
|
|
2267
2279
|
}
|
|
2268
2280
|
function ensureConfigDir() {
|
|
2269
2281
|
const dir = getConfigDir();
|
|
@@ -2290,7 +2302,7 @@ function normalizeAdapterIds(input) {
|
|
|
2290
2302
|
}
|
|
2291
2303
|
function expandHomePath(p) {
|
|
2292
2304
|
if (p.startsWith("~/") || p === "~")
|
|
2293
|
-
return join7(
|
|
2305
|
+
return join7(getHomeDir(), p.slice(1));
|
|
2294
2306
|
return p;
|
|
2295
2307
|
}
|
|
2296
2308
|
function resolveCustomPlanDirPath(userPath) {
|
|
@@ -2438,8 +2450,335 @@ var CLI_DAEMON_STALE_AFTER_MS = CLI_DAEMON_HEARTBEAT_INTERVAL_MS * 5;
|
|
|
2438
2450
|
// ../shared/src/services/plan-service.ts
|
|
2439
2451
|
import { existsSync as existsSync4, readdirSync as readdirSync3, statSync } from "node:fs";
|
|
2440
2452
|
import { lstat, mkdir, readdir as readdir2, readFile as readFile6, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
|
|
2441
|
-
import { homedir as homedir8 } from "node:os";
|
|
2442
2453
|
import { join as join8, resolve as resolve3, sep as sep2 } from "node:path";
|
|
2454
|
+
|
|
2455
|
+
// ../shared/src/services/plan-value.ts
|
|
2456
|
+
function isLowValuePlan(plan) {
|
|
2457
|
+
const metadata = plan.metadata;
|
|
2458
|
+
return typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) && metadata.lowValue === true;
|
|
2459
|
+
}
|
|
2460
|
+
function isIndexablePlan(plan) {
|
|
2461
|
+
return !isLowValuePlan(plan);
|
|
2462
|
+
}
|
|
2463
|
+
var PROPOSED_PLAN_TAG_REGEX2 = /<\s*\/?\s*proposed_plan\s*>/gi;
|
|
2464
|
+
var ESCAPED_PROPOSED_PLAN_TAG_REGEX2 = /<\s*\/?\s*proposed_plan\s*>/gi;
|
|
2465
|
+
var VISIBLE_TEXT_REGEX = /[\p{L}\p{N}]/u;
|
|
2466
|
+
var LOW_VALUE_METADATA_KEYS = ["lowValue", "lowValueReasons", "lowValueSignals"];
|
|
2467
|
+
function normalizeLineEndings2(text) {
|
|
2468
|
+
return text.replace(/\r\n?/g, `
|
|
2469
|
+
`);
|
|
2470
|
+
}
|
|
2471
|
+
function stripBoundaryHtmlComments(text) {
|
|
2472
|
+
let next = text;
|
|
2473
|
+
let previous = "";
|
|
2474
|
+
while (next !== previous) {
|
|
2475
|
+
previous = next;
|
|
2476
|
+
next = next.replace(/^\s*<!--[\s\S]*?-->\s*/, "").replace(/\s*<!--[\s\S]*?-->\s*$/, "");
|
|
2477
|
+
}
|
|
2478
|
+
return next;
|
|
2479
|
+
}
|
|
2480
|
+
function normalizePlanContent(content) {
|
|
2481
|
+
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();
|
|
2482
|
+
}
|
|
2483
|
+
function withoutLowValueMetadata(metadata) {
|
|
2484
|
+
const next = { ...metadata ?? {} };
|
|
2485
|
+
for (const key of LOW_VALUE_METADATA_KEYS)
|
|
2486
|
+
delete next[key];
|
|
2487
|
+
return next;
|
|
2488
|
+
}
|
|
2489
|
+
function unique(items) {
|
|
2490
|
+
return Array.from(new Set(items));
|
|
2491
|
+
}
|
|
2492
|
+
function visibleText(text) {
|
|
2493
|
+
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();
|
|
2494
|
+
}
|
|
2495
|
+
function isSeparatorLine(line) {
|
|
2496
|
+
return /^(?:-{3,}|_{3,}|\*{3,})$/.test(line.trim());
|
|
2497
|
+
}
|
|
2498
|
+
function isFenceLine(line) {
|
|
2499
|
+
return /^(?:`{3,}|~{3,})/.test(line.trim());
|
|
2500
|
+
}
|
|
2501
|
+
function meaningfulLines(text) {
|
|
2502
|
+
return text.split(`
|
|
2503
|
+
`).map((line) => line.trim()).filter((line) => line && !isSeparatorLine(line) && !isFenceLine(line));
|
|
2504
|
+
}
|
|
2505
|
+
function cleanMarkdownLine(line) {
|
|
2506
|
+
return line.trim().replace(/^>+\s*/, "").replace(/^#{1,6}\s+/, "").replace(/^[-*+]\s+/, "").replace(/^\d+[.)]\s+/, "").replace(/^\[[ xX]\]\s+/, "").replace(/^\*\*|\*\*$/g, "").replace(/^`|`$/g, "").trim();
|
|
2507
|
+
}
|
|
2508
|
+
function isHeadingLine(line) {
|
|
2509
|
+
return /^#{1,6}\s+\S/.test(line.trim());
|
|
2510
|
+
}
|
|
2511
|
+
function isChecklistLine(line) {
|
|
2512
|
+
return /^[-*+]\s+\[[ xX]\]\s+\S/.test(line.trim());
|
|
2513
|
+
}
|
|
2514
|
+
function isOrderedListLine(line) {
|
|
2515
|
+
return /^\d+[.)]\s+\S/.test(line.trim());
|
|
2516
|
+
}
|
|
2517
|
+
function isHeadingOnly(lines) {
|
|
2518
|
+
return lines.length > 0 && lines.some(isHeadingLine) && lines.every(isHeadingLine);
|
|
2519
|
+
}
|
|
2520
|
+
function metadataHasPlanBlocks(metadata) {
|
|
2521
|
+
const planBlocks = metadata?.planBlocks;
|
|
2522
|
+
return typeof planBlocks === "number" && planBlocks > 0;
|
|
2523
|
+
}
|
|
2524
|
+
function sectionName(line) {
|
|
2525
|
+
return cleanMarkdownLine(line).replace(/:$/, "").toLowerCase().replace(/\s+/g, " ");
|
|
2526
|
+
}
|
|
2527
|
+
var SECTION_LABELS = new Set([
|
|
2528
|
+
"context",
|
|
2529
|
+
"background",
|
|
2530
|
+
"problem",
|
|
2531
|
+
"goal",
|
|
2532
|
+
"goals",
|
|
2533
|
+
"scope",
|
|
2534
|
+
"approach",
|
|
2535
|
+
"strategy",
|
|
2536
|
+
"design",
|
|
2537
|
+
"implementation plan",
|
|
2538
|
+
"implementation",
|
|
2539
|
+
"plan",
|
|
2540
|
+
"files to modify",
|
|
2541
|
+
"files changed",
|
|
2542
|
+
"affected files",
|
|
2543
|
+
"steps",
|
|
2544
|
+
"step",
|
|
2545
|
+
"tasks",
|
|
2546
|
+
"task",
|
|
2547
|
+
"checklist",
|
|
2548
|
+
"todo",
|
|
2549
|
+
"todos",
|
|
2550
|
+
"verification",
|
|
2551
|
+
"testing",
|
|
2552
|
+
"tests",
|
|
2553
|
+
"test",
|
|
2554
|
+
"validation",
|
|
2555
|
+
"reuse",
|
|
2556
|
+
"existing utilities",
|
|
2557
|
+
"existing code",
|
|
2558
|
+
"acceptance criteria",
|
|
2559
|
+
"success criteria"
|
|
2560
|
+
]);
|
|
2561
|
+
function hasSectionSyntax(line, section) {
|
|
2562
|
+
const cleaned = cleanMarkdownLine(line);
|
|
2563
|
+
return isHeadingLine(line) || /^[a-z][\w\s/&+-]{1,48}:/i.test(cleaned) || SECTION_LABELS.has(section);
|
|
2564
|
+
}
|
|
2565
|
+
function collectPositiveSignals(normalized, lines, metadata) {
|
|
2566
|
+
const signals = [];
|
|
2567
|
+
if (metadataHasPlanBlocks(metadata))
|
|
2568
|
+
signals.push("metadata:proposed-plan-block");
|
|
2569
|
+
for (const line of lines) {
|
|
2570
|
+
const section = sectionName(line);
|
|
2571
|
+
if (!hasSectionSyntax(line, section))
|
|
2572
|
+
continue;
|
|
2573
|
+
if (/^(context|background|problem|goal|goals|scope)\b/.test(section)) {
|
|
2574
|
+
signals.push("section:context");
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
if (/^(approach|strategy|design)\b/.test(section)) {
|
|
2578
|
+
signals.push("section:approach");
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (/^(implementation plan|implementation|plan)\b/.test(section)) {
|
|
2582
|
+
signals.push("section:implementation-plan");
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
if (/^(files? to modify|files? changed|affected files?)\b/.test(section)) {
|
|
2586
|
+
signals.push("section:files-to-modify");
|
|
2587
|
+
continue;
|
|
2588
|
+
}
|
|
2589
|
+
if (/^(steps?|tasks?|checklist|todo|todos)\b/.test(section)) {
|
|
2590
|
+
signals.push("section:steps");
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
if (/^(verification|testing|tests?|validation)\b/.test(section)) {
|
|
2594
|
+
signals.push("section:verification");
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
if (/^(reuse|existing utilities|existing code)\b/.test(section)) {
|
|
2598
|
+
signals.push("section:reuse");
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (/^(acceptance criteria|success criteria)\b/.test(section)) {
|
|
2602
|
+
signals.push("section:acceptance-criteria");
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
if (lines.some(isChecklistLine))
|
|
2606
|
+
signals.push("checklist");
|
|
2607
|
+
const orderedStepCount = lines.filter(isOrderedListLine).length;
|
|
2608
|
+
if (orderedStepCount >= 2)
|
|
2609
|
+
signals.push("ordered-steps");
|
|
2610
|
+
const actionBulletCount = lines.filter((line) => {
|
|
2611
|
+
const trimmed = line.trim();
|
|
2612
|
+
if (!/^(?:[-*+]|\d+[.)])\s+/.test(trimmed))
|
|
2613
|
+
return false;
|
|
2614
|
+
const cleaned = cleanMarkdownLine(trimmed);
|
|
2615
|
+
return /^(?:add|implement|update|modify|create|remove|delete|refactor|test|verify|run|wire|persist|handle|ensure|document|rename|move|extract|reuse|validate)\b/i.test(cleaned);
|
|
2616
|
+
}).length;
|
|
2617
|
+
if (actionBulletCount >= 2)
|
|
2618
|
+
signals.push("action-bullets");
|
|
2619
|
+
if (lines.length >= 2 && /\b(?:will|should|need to|needs to|plan to|planned|approach is to|implementation will)\b/i.test(normalized)) {
|
|
2620
|
+
signals.push("future-plan-language");
|
|
2621
|
+
}
|
|
2622
|
+
return unique(signals);
|
|
2623
|
+
}
|
|
2624
|
+
function isStrongPositiveSignal(signal) {
|
|
2625
|
+
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";
|
|
2626
|
+
}
|
|
2627
|
+
function hasStrongPlanSignal(positiveSignals) {
|
|
2628
|
+
const sectionCount = positiveSignals.filter((signal) => signal.startsWith("section:")).length;
|
|
2629
|
+
return positiveSignals.some(isStrongPositiveSignal) || sectionCount >= 2;
|
|
2630
|
+
}
|
|
2631
|
+
function isPromptLikeOneLiner(line) {
|
|
2632
|
+
if (isHeadingLine(line) || isChecklistLine(line) || isOrderedListLine(line))
|
|
2633
|
+
return false;
|
|
2634
|
+
const cleaned = cleanMarkdownLine(line).toLowerCase();
|
|
2635
|
+
if (!cleaned)
|
|
2636
|
+
return false;
|
|
2637
|
+
return [
|
|
2638
|
+
/^(?:important:\s*)?work in\b/,
|
|
2639
|
+
/^(?:please|pls)\b/,
|
|
2640
|
+
/^(?:can|could|would|will)\s+you\b/,
|
|
2641
|
+
/^(?:i|we)\s+(?:need|want|would like|have to)\b/,
|
|
2642
|
+
/^(?:help|fix|implement|create|add|update|remove|delete|refactor|write|review|investigate|debug|plan)\b/,
|
|
2643
|
+
/\?$/,
|
|
2644
|
+
/\b(?:repository|repo|existing branch|worktree|pull request|pr)\b/
|
|
2645
|
+
].some((pattern) => pattern.test(cleaned));
|
|
2646
|
+
}
|
|
2647
|
+
function normalizedTitle(title) {
|
|
2648
|
+
return cleanMarkdownLine(title ?? "").toLowerCase();
|
|
2649
|
+
}
|
|
2650
|
+
function looksLikeWrapperTitle(title) {
|
|
2651
|
+
return /^<user_(?:action|instructions|prompt)>$/i.test((title ?? "").trim());
|
|
2652
|
+
}
|
|
2653
|
+
function looksLikePromptTitle(title) {
|
|
2654
|
+
const cleaned = normalizedTitle(title);
|
|
2655
|
+
if (!cleaned)
|
|
2656
|
+
return false;
|
|
2657
|
+
return [
|
|
2658
|
+
/^(?:important:\s*)?work in\b/,
|
|
2659
|
+
/^review the code changes against\b/,
|
|
2660
|
+
/^perform a .*review\b/,
|
|
2661
|
+
/^(?:please|pls)\b/,
|
|
2662
|
+
/^(?:can|could|would|will)\s+you\b/,
|
|
2663
|
+
/^(?:i|we)\s+(?:need|want|would like|have to)\b/,
|
|
2664
|
+
/^(?:help|fix|implement|create|add|update|remove|delete|refactor|write|review|investigate|debug|plan)\b/,
|
|
2665
|
+
/\?$/,
|
|
2666
|
+
/\b(?:repository|repo|existing branch|worktree|pull request|pr)\b/
|
|
2667
|
+
].some((pattern) => pattern.test(cleaned));
|
|
2668
|
+
}
|
|
2669
|
+
function looksLikeReviewOutput(normalized, title) {
|
|
2670
|
+
const cleanedTitle = normalizedTitle(title);
|
|
2671
|
+
const lower = normalized.toLowerCase();
|
|
2672
|
+
return /^review the code changes against\b/.test(cleanedTitle) || /^perform a .*review\b/.test(cleanedTitle) || /"findings"\s*:\s*\[/.test(normalized) || /"overall_correctness"\s*:/.test(normalized) || /\bfull review comments\s*:/i.test(normalized) || /\bthe patch (?:currently )?(?:breaks|introduces|regresses)\b/i.test(normalized) || /\bshould not be considered correct\b/i.test(lower);
|
|
2673
|
+
}
|
|
2674
|
+
function looksLikeSystemContext(normalized, lines) {
|
|
2675
|
+
const lower = normalized.toLowerCase();
|
|
2676
|
+
if (lower.startsWith("# agents.md instructions") || lower.startsWith("<environment_context>") || lower.startsWith("<system-reminder>") || lower.startsWith("<thinking>")) {
|
|
2677
|
+
return true;
|
|
2678
|
+
}
|
|
2679
|
+
const wrapperMatches = normalized.match(/<\/?(?:environment_context|system-reminder|thinking|analysis|reasoning|tool_call|tool_result)\b[^>]*>/gi);
|
|
2680
|
+
if (wrapperMatches && wrapperMatches.length >= 2)
|
|
2681
|
+
return true;
|
|
2682
|
+
const wrapperLineCount = lines.filter((line) => /^(?:analysis|reasoning|thought|assistant thought|system|developer):\b/i.test(line)).length;
|
|
2683
|
+
return wrapperLineCount > 0 && wrapperLineCount / Math.max(lines.length, 1) >= 0.4;
|
|
2684
|
+
}
|
|
2685
|
+
function looksLikeToolLog(normalized) {
|
|
2686
|
+
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);
|
|
2687
|
+
}
|
|
2688
|
+
function looksLikeExecutionReport(normalized) {
|
|
2689
|
+
const hasPastCompletion = /\b(?:fixed|pushed|committed|completed|done|implemented|updated|changed|patched|merged|deployed|passed|failed|resolved|reverted)\b/i.test(normalized);
|
|
2690
|
+
const hasReportSection = /^\s*(?:summary|result|results|changes|verification|status)\s*:/im.test(normalized);
|
|
2691
|
+
const hasReviewReportMarker = /\b(?:review findings?|review issues?|review comments?)\b/i.test(normalized);
|
|
2692
|
+
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);
|
|
2693
|
+
return hasPastCompletion && (hasReportSection || hasCommandMarker || hasReviewReportMarker);
|
|
2694
|
+
}
|
|
2695
|
+
function lowValueAssessment(reasons, signals) {
|
|
2696
|
+
return {
|
|
2697
|
+
lowValue: reasons.length > 0,
|
|
2698
|
+
reasons: unique(reasons),
|
|
2699
|
+
signals: unique(signals).slice(0, 20)
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
function assessPlanValue(input) {
|
|
2703
|
+
const metadata = withoutLowValueMetadata(input.metadata);
|
|
2704
|
+
const normalized = normalizePlanContent(input.content);
|
|
2705
|
+
const lines = meaningfulLines(normalized);
|
|
2706
|
+
const positiveSignals = collectPositiveSignals(normalized, lines, metadata);
|
|
2707
|
+
const signals = [...positiveSignals];
|
|
2708
|
+
const reasons = [];
|
|
2709
|
+
if (!VISIBLE_TEXT_REGEX.test(visibleText(normalized))) {
|
|
2710
|
+
return lowValueAssessment(["empty-content"], ["negative:empty-content"]);
|
|
2711
|
+
}
|
|
2712
|
+
if (isHeadingOnly(lines)) {
|
|
2713
|
+
return lowValueAssessment(["heading-only"], [...signals, "negative:heading-only"]);
|
|
2714
|
+
}
|
|
2715
|
+
const explicitPlanBlock = metadataHasPlanBlocks(metadata);
|
|
2716
|
+
const strongPositive = hasStrongPlanSignal(positiveSignals);
|
|
2717
|
+
const systemContext = looksLikeSystemContext(normalized, lines);
|
|
2718
|
+
const toolLog = looksLikeToolLog(normalized);
|
|
2719
|
+
const executionReport = looksLikeExecutionReport(normalized);
|
|
2720
|
+
const wrapperTitle = looksLikeWrapperTitle(input.title);
|
|
2721
|
+
const promptTitle = looksLikePromptTitle(input.title);
|
|
2722
|
+
const reviewOutput = looksLikeReviewOutput(normalized, input.title);
|
|
2723
|
+
if (systemContext)
|
|
2724
|
+
signals.push("negative:system-context");
|
|
2725
|
+
if (toolLog)
|
|
2726
|
+
signals.push("negative:tool-log");
|
|
2727
|
+
if (executionReport)
|
|
2728
|
+
signals.push("negative:execution-report");
|
|
2729
|
+
if (wrapperTitle)
|
|
2730
|
+
signals.push("negative:wrapper-title");
|
|
2731
|
+
if (promptTitle)
|
|
2732
|
+
signals.push("negative:prompt-title");
|
|
2733
|
+
if (reviewOutput)
|
|
2734
|
+
signals.push("negative:review-output");
|
|
2735
|
+
if (lines.length === 1)
|
|
2736
|
+
signals.push("shape:single-line");
|
|
2737
|
+
if (lines.length === 1 && positiveSignals.length === 0) {
|
|
2738
|
+
reasons.push(isPromptLikeOneLiner(lines[0] ?? "") ? "prompt-like" : "no-plan-signals");
|
|
2739
|
+
}
|
|
2740
|
+
if (systemContext && !strongPositive)
|
|
2741
|
+
reasons.push("system-context");
|
|
2742
|
+
if (executionReport && !strongPositive)
|
|
2743
|
+
reasons.push("execution-report");
|
|
2744
|
+
if (wrapperTitle && !explicitPlanBlock)
|
|
2745
|
+
reasons.push("wrapper-title");
|
|
2746
|
+
if (reviewOutput && !explicitPlanBlock)
|
|
2747
|
+
reasons.push("review-output");
|
|
2748
|
+
if (promptTitle && !strongPositive && positiveSignals.length === 0)
|
|
2749
|
+
reasons.push("prompt-like");
|
|
2750
|
+
if (toolLog && !strongPositive && positiveSignals.length === 0)
|
|
2751
|
+
reasons.push("no-plan-signals");
|
|
2752
|
+
if (reasons.length === 0 && positiveSignals.length === 0 && lines.length <= 3) {
|
|
2753
|
+
reasons.push("no-plan-signals");
|
|
2754
|
+
}
|
|
2755
|
+
return lowValueAssessment(reasons, signals);
|
|
2756
|
+
}
|
|
2757
|
+
function annotatePlanValueMetadata(plan) {
|
|
2758
|
+
const baseMetadata = withoutLowValueMetadata(plan.metadata);
|
|
2759
|
+
const assessment = assessPlanValue({
|
|
2760
|
+
content: plan.content,
|
|
2761
|
+
title: plan.title,
|
|
2762
|
+
metadata: baseMetadata
|
|
2763
|
+
});
|
|
2764
|
+
if (!assessment.lowValue) {
|
|
2765
|
+
return {
|
|
2766
|
+
...plan,
|
|
2767
|
+
metadata: baseMetadata
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
return {
|
|
2771
|
+
...plan,
|
|
2772
|
+
metadata: {
|
|
2773
|
+
...baseMetadata,
|
|
2774
|
+
lowValue: true,
|
|
2775
|
+
lowValueReasons: assessment.reasons,
|
|
2776
|
+
lowValueSignals: assessment.signals
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// ../shared/src/services/plan-service.ts
|
|
2443
2782
|
function getUserPlansDir() {
|
|
2444
2783
|
return join8(getConfigDir(), "plans");
|
|
2445
2784
|
}
|
|
@@ -2480,7 +2819,7 @@ var SKIP_DIRS = new Set([
|
|
|
2480
2819
|
"iCloud Drive"
|
|
2481
2820
|
]);
|
|
2482
2821
|
function discoverProjectPlanDirs() {
|
|
2483
|
-
const home2 =
|
|
2822
|
+
const home2 = getHomeDir();
|
|
2484
2823
|
const results = [];
|
|
2485
2824
|
function walk(dir, depth) {
|
|
2486
2825
|
if (depth > DISCOVERY_MAX_DEPTH)
|
|
@@ -2552,7 +2891,7 @@ async function parseGenericMarkdownPlan(filePath, extraMetadata) {
|
|
|
2552
2891
|
try {
|
|
2553
2892
|
const content = await readFile6(filePath, "utf-8");
|
|
2554
2893
|
const stats = await stat6(filePath);
|
|
2555
|
-
let agent = "unknown";
|
|
2894
|
+
let agent = (typeof extraMetadata.agentHint === "string" ? extraMetadata.agentHint : "") || "unknown";
|
|
2556
2895
|
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
2557
2896
|
if (fmMatch) {
|
|
2558
2897
|
const agentLine = fmMatch[1]?.match(/^agent:\s*(.+)$/m);
|
|
@@ -2630,13 +2969,15 @@ async function scanCustomPlanDirs(coveredPaths, into) {
|
|
|
2630
2969
|
continue;
|
|
2631
2970
|
}
|
|
2632
2971
|
const files = await walkDir(dir);
|
|
2972
|
+
const dirBasename = dir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "custom";
|
|
2633
2973
|
let count = 0;
|
|
2634
2974
|
for (const file of files) {
|
|
2635
2975
|
if (!file.endsWith(".md"))
|
|
2636
2976
|
continue;
|
|
2637
2977
|
const plan = await parseGenericMarkdownPlan(file, {
|
|
2638
2978
|
source: "custom-dir",
|
|
2639
|
-
customDir: dir
|
|
2979
|
+
customDir: dir,
|
|
2980
|
+
agentHint: dirBasename
|
|
2640
2981
|
});
|
|
2641
2982
|
if (plan) {
|
|
2642
2983
|
into.set(plan.id, plan);
|
|
@@ -2659,7 +3000,8 @@ async function scan() {
|
|
|
2659
3000
|
continue;
|
|
2660
3001
|
const plans = await adapter.parse(file);
|
|
2661
3002
|
for (const plan of plans) {
|
|
2662
|
-
|
|
3003
|
+
const annotated = annotatePlanValueMetadata(plan);
|
|
3004
|
+
next.set(annotated.id, annotated);
|
|
2663
3005
|
}
|
|
2664
3006
|
}
|
|
2665
3007
|
}
|
|
@@ -2678,7 +3020,8 @@ async function scan() {
|
|
|
2678
3020
|
continue;
|
|
2679
3021
|
const plans = await adapter.parse(file);
|
|
2680
3022
|
for (const plan of plans) {
|
|
2681
|
-
|
|
3023
|
+
const annotated = annotatePlanValueMetadata(plan);
|
|
3024
|
+
next.set(annotated.id, annotated);
|
|
2682
3025
|
}
|
|
2683
3026
|
}
|
|
2684
3027
|
coveredPaths.add(resolvedDir);
|
|
@@ -2688,11 +3031,17 @@ async function scan() {
|
|
|
2688
3031
|
await scanCustomPlanDirs(coveredPaths, next);
|
|
2689
3032
|
store = next;
|
|
2690
3033
|
notifyPlansChanged();
|
|
2691
|
-
|
|
3034
|
+
const indexableCount = getIndexablePlans().length;
|
|
3035
|
+
const hiddenCount = store.size - indexableCount;
|
|
3036
|
+
const hiddenSuffix = hiddenCount > 0 ? ` (${hiddenCount} hidden as low-value)` : "";
|
|
3037
|
+
console.log(`[agendex] indexed ${indexableCount} plans${hiddenSuffix} from ${adapters.length} adapters`);
|
|
2692
3038
|
}
|
|
2693
3039
|
function getAll() {
|
|
2694
3040
|
return Array.from(store.values());
|
|
2695
3041
|
}
|
|
3042
|
+
function getIndexablePlans() {
|
|
3043
|
+
return getAll().filter(isIndexablePlan);
|
|
3044
|
+
}
|
|
2696
3045
|
async function rescanFile(filePath) {
|
|
2697
3046
|
const adapters = getActiveAdapters();
|
|
2698
3047
|
const normalized = resolve3(filePath);
|
|
@@ -2707,7 +3056,7 @@ async function rescanFile(filePath) {
|
|
|
2707
3056
|
const isInSearchPath = allSearchPaths.some((sp) => normalized.startsWith(sp + sep2) || normalized === sp);
|
|
2708
3057
|
if (!isInSearchPath)
|
|
2709
3058
|
continue;
|
|
2710
|
-
const plans = await adapter.parse(filePath);
|
|
3059
|
+
const plans = (await adapter.parse(filePath)).map(annotatePlanValueMetadata);
|
|
2711
3060
|
for (const plan of plans) {
|
|
2712
3061
|
store.set(plan.id, plan);
|
|
2713
3062
|
}
|
|
@@ -2905,6 +3254,22 @@ function getCloudConfig() {
|
|
|
2905
3254
|
convexUrl: config.convexUrl
|
|
2906
3255
|
};
|
|
2907
3256
|
}
|
|
3257
|
+
function parseSyncSuccess(body) {
|
|
3258
|
+
try {
|
|
3259
|
+
const parsed = JSON.parse(body);
|
|
3260
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3261
|
+
return { ok: true };
|
|
3262
|
+
}
|
|
3263
|
+
const result = parsed;
|
|
3264
|
+
return {
|
|
3265
|
+
ok: true,
|
|
3266
|
+
skippedLowValue: result.skippedLowValue === true,
|
|
3267
|
+
deleted: result.deleted === true
|
|
3268
|
+
};
|
|
3269
|
+
} catch {
|
|
3270
|
+
return { ok: true };
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
2908
3273
|
async function syncPlan(plan) {
|
|
2909
3274
|
const { token, convexUrl } = getCloudConfig();
|
|
2910
3275
|
const url = `${convexUrl}/api/cli/sync`;
|
|
@@ -2936,7 +3301,7 @@ async function syncPlan(plan) {
|
|
|
2936
3301
|
if (res.status < 200 || res.status >= 300) {
|
|
2937
3302
|
return { ok: false, error: `${res.status}: ${res.body}` };
|
|
2938
3303
|
}
|
|
2939
|
-
return
|
|
3304
|
+
return parseSyncSuccess(res.body);
|
|
2940
3305
|
}
|
|
2941
3306
|
async function refreshStoredToken(currentToken, convexUrl) {
|
|
2942
3307
|
const refreshed = await refreshToken(currentToken, convexUrl);
|
|
@@ -2991,7 +3356,6 @@ async function sendHeartbeat() {
|
|
|
2991
3356
|
}
|
|
2992
3357
|
async function sendShutdown() {
|
|
2993
3358
|
try {
|
|
2994
|
-
const { token, convexUrl } = getCloudConfig();
|
|
2995
3359
|
cachedDeviceId ??= loadOrCreateDeviceId();
|
|
2996
3360
|
await deleteDaemons([cachedDeviceId]);
|
|
2997
3361
|
} catch {}
|
|
@@ -3409,6 +3773,8 @@ async function runWorker() {
|
|
|
3409
3773
|
syncing = true;
|
|
3410
3774
|
const batch = syncQueue.splice(0);
|
|
3411
3775
|
let syncedCount = 0;
|
|
3776
|
+
let lowValueSkippedCount = 0;
|
|
3777
|
+
let lowValueDeletedCount = 0;
|
|
3412
3778
|
let failedCount = 0;
|
|
3413
3779
|
try {
|
|
3414
3780
|
for (const payload of batch) {
|
|
@@ -3429,19 +3795,26 @@ async function runWorker() {
|
|
|
3429
3795
|
failedCount++;
|
|
3430
3796
|
console.error(`[agendex] sync failed for "${payload.title}": ${result.error}`);
|
|
3431
3797
|
} else {
|
|
3432
|
-
|
|
3798
|
+
if (result.skippedLowValue) {
|
|
3799
|
+
lowValueSkippedCount++;
|
|
3800
|
+
if (result.deleted)
|
|
3801
|
+
lowValueDeletedCount++;
|
|
3802
|
+
} else {
|
|
3803
|
+
syncedCount++;
|
|
3804
|
+
}
|
|
3433
3805
|
syncCache[payload.localPlanId] = computePayloadHash(payload);
|
|
3434
3806
|
}
|
|
3435
3807
|
}
|
|
3436
3808
|
} catch (err) {
|
|
3437
3809
|
console.error("[agendex] sync error:", err);
|
|
3438
|
-
syncQueue.unshift(...batch.slice(syncedCount + failedCount));
|
|
3810
|
+
syncQueue.unshift(...batch.slice(syncedCount + lowValueSkippedCount + failedCount));
|
|
3439
3811
|
} finally {
|
|
3440
3812
|
syncing = false;
|
|
3441
3813
|
}
|
|
3442
|
-
if (syncedCount > 0 || failedCount > 0) {
|
|
3814
|
+
if (syncedCount > 0 || lowValueSkippedCount > 0 || failedCount > 0) {
|
|
3443
3815
|
saveSyncCache(syncCache);
|
|
3444
|
-
|
|
3816
|
+
const lowValueSuffix2 = lowValueSkippedCount > 0 ? `, ${lowValueSkippedCount} low-value skipped/pruned${lowValueDeletedCount > 0 ? ` (${lowValueDeletedCount} deleted)` : ""}` : "";
|
|
3817
|
+
console.log(`[agendex] sync complete: ${syncedCount} synced${lowValueSuffix2}, ${failedCount} failed`);
|
|
3445
3818
|
}
|
|
3446
3819
|
if (syncQueue.length > 0)
|
|
3447
3820
|
processSyncQueue();
|
|
@@ -3450,7 +3823,11 @@ async function runWorker() {
|
|
|
3450
3823
|
console.log(`[agendex] initial scan...`);
|
|
3451
3824
|
await scan();
|
|
3452
3825
|
const plans = getAll();
|
|
3826
|
+
const syncablePlanCount = plans.filter(isIndexablePlan).length;
|
|
3827
|
+
const lowValuePlanCount = plans.length - syncablePlanCount;
|
|
3453
3828
|
let initialSkipped = 0;
|
|
3829
|
+
let initialQueuedSyncable = 0;
|
|
3830
|
+
let initialQueuedLowValue = 0;
|
|
3454
3831
|
for (const plan of plans) {
|
|
3455
3832
|
const payload = planToPayload(plan);
|
|
3456
3833
|
const hash = computePayloadHash(payload);
|
|
@@ -3458,6 +3835,11 @@ async function runWorker() {
|
|
|
3458
3835
|
initialSkipped++;
|
|
3459
3836
|
continue;
|
|
3460
3837
|
}
|
|
3838
|
+
if (isLowValuePlan(plan)) {
|
|
3839
|
+
initialQueuedLowValue++;
|
|
3840
|
+
} else {
|
|
3841
|
+
initialQueuedSyncable++;
|
|
3842
|
+
}
|
|
3461
3843
|
syncQueue.push(payload);
|
|
3462
3844
|
}
|
|
3463
3845
|
const activePlanIds = new Set(plans.map((plan) => plan.id));
|
|
@@ -3466,7 +3848,8 @@ async function runWorker() {
|
|
|
3466
3848
|
delete syncCache[id];
|
|
3467
3849
|
}
|
|
3468
3850
|
saveSyncCache(syncCache, { replace: true });
|
|
3469
|
-
|
|
3851
|
+
const lowValueSuffix = lowValuePlanCount > 0 ? `, ${lowValuePlanCount} low-value hidden/pruned` : "";
|
|
3852
|
+
console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
|
|
3470
3853
|
await processSyncQueue();
|
|
3471
3854
|
setInterval(() => void sendHeartbeat(), CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
|
|
3472
3855
|
startWatching((changedPlans) => {
|
|
@@ -3534,33 +3917,41 @@ async function startSupervisor() {
|
|
|
3534
3917
|
}
|
|
3535
3918
|
|
|
3536
3919
|
// src/sync.ts
|
|
3920
|
+
function planToPayload2(plan) {
|
|
3921
|
+
return {
|
|
3922
|
+
localPlanId: plan.id,
|
|
3923
|
+
agent: plan.agent,
|
|
3924
|
+
title: plan.title,
|
|
3925
|
+
content: plan.content,
|
|
3926
|
+
format: plan.format,
|
|
3927
|
+
filePath: plan.filePath,
|
|
3928
|
+
workspace: plan.workspace,
|
|
3929
|
+
metadata: plan.metadata,
|
|
3930
|
+
createdAt: plan.createdAt.getTime(),
|
|
3931
|
+
updatedAt: plan.updatedAt.getTime()
|
|
3932
|
+
};
|
|
3933
|
+
}
|
|
3537
3934
|
async function syncAll(force = false) {
|
|
3538
3935
|
const config = await loadOrInitConfig();
|
|
3539
3936
|
const adapters = resolveAdapters(config.enabledAdapters);
|
|
3540
3937
|
setActiveAdapters(adapters);
|
|
3541
3938
|
console.log(`[agendex] Scanning local plans...`);
|
|
3542
3939
|
await scan();
|
|
3543
|
-
const
|
|
3544
|
-
|
|
3940
|
+
const allPlans = getAll();
|
|
3941
|
+
const syncablePlans = allPlans.filter(isIndexablePlan);
|
|
3942
|
+
const lowValuePlans = allPlans.filter(isLowValuePlan);
|
|
3943
|
+
const hiddenSuffix = lowValuePlans.length > 0 ? ` (${lowValuePlans.length} low-value hidden/pruned)` : "";
|
|
3944
|
+
console.log(`[agendex] Found ${syncablePlans.length} syncable plans${hiddenSuffix}. Syncing to cloud...`);
|
|
3545
3945
|
const cache = force ? {} : loadSyncCache();
|
|
3546
3946
|
const activePlanIds = new Set;
|
|
3547
3947
|
let synced = 0;
|
|
3948
|
+
let lowValueSkipped = 0;
|
|
3949
|
+
let lowValueDeleted = 0;
|
|
3548
3950
|
let skipped = 0;
|
|
3549
3951
|
let failed = 0;
|
|
3550
|
-
for (const plan of
|
|
3952
|
+
for (const plan of [...syncablePlans, ...lowValuePlans]) {
|
|
3551
3953
|
activePlanIds.add(plan.id);
|
|
3552
|
-
const payload =
|
|
3553
|
-
localPlanId: plan.id,
|
|
3554
|
-
agent: plan.agent,
|
|
3555
|
-
title: plan.title,
|
|
3556
|
-
content: plan.content,
|
|
3557
|
-
format: plan.format,
|
|
3558
|
-
filePath: plan.filePath,
|
|
3559
|
-
workspace: plan.workspace,
|
|
3560
|
-
metadata: plan.metadata,
|
|
3561
|
-
createdAt: plan.createdAt.getTime(),
|
|
3562
|
-
updatedAt: plan.updatedAt.getTime()
|
|
3563
|
-
};
|
|
3954
|
+
const payload = planToPayload2(plan);
|
|
3564
3955
|
const hash = computePayloadHash(payload);
|
|
3565
3956
|
if (!force && cache[plan.id] === hash) {
|
|
3566
3957
|
skipped++;
|
|
@@ -3568,7 +3959,13 @@ async function syncAll(force = false) {
|
|
|
3568
3959
|
}
|
|
3569
3960
|
const result = await syncPlan(payload);
|
|
3570
3961
|
if (result.ok) {
|
|
3571
|
-
|
|
3962
|
+
if (result.skippedLowValue) {
|
|
3963
|
+
lowValueSkipped++;
|
|
3964
|
+
if (result.deleted)
|
|
3965
|
+
lowValueDeleted++;
|
|
3966
|
+
} else {
|
|
3967
|
+
synced++;
|
|
3968
|
+
}
|
|
3572
3969
|
cache[plan.id] = hash;
|
|
3573
3970
|
} else {
|
|
3574
3971
|
failed++;
|
|
@@ -3580,7 +3977,8 @@ async function syncAll(force = false) {
|
|
|
3580
3977
|
delete cache[id];
|
|
3581
3978
|
}
|
|
3582
3979
|
saveSyncCache(cache, { replace: true });
|
|
3583
|
-
|
|
3980
|
+
const lowValueSuffix = lowValueSkipped > 0 ? `, ${lowValueSkipped} low-value skipped/pruned${lowValueDeleted > 0 ? ` (${lowValueDeleted} deleted)` : ""}` : "";
|
|
3981
|
+
console.log(`[agendex] Sync complete: ${synced} synced${lowValueSuffix}, ${skipped} unchanged, ${failed} failed`);
|
|
3584
3982
|
}
|
|
3585
3983
|
|
|
3586
3984
|
// src/version.ts
|
|
@@ -3590,7 +3988,7 @@ import { join as join12 } from "node:path";
|
|
|
3590
3988
|
// package.json
|
|
3591
3989
|
var package_default = {
|
|
3592
3990
|
name: "agendex-cli",
|
|
3593
|
-
version: "0.
|
|
3991
|
+
version: "0.14.0",
|
|
3594
3992
|
description: "Agendex CLI for login, sync, and daemon workflows",
|
|
3595
3993
|
homepage: "https://github.com/Tyru5/Agendex#readme",
|
|
3596
3994
|
repository: {
|
|
@@ -3945,15 +4343,61 @@ async function main() {
|
|
|
3945
4343
|
writeStderr(`[agendex] path is not a directory: ${resolved}`);
|
|
3946
4344
|
return 1;
|
|
3947
4345
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
4346
|
+
if (args.includes("--live")) {
|
|
4347
|
+
const cfg = loadConfig();
|
|
4348
|
+
const token = cfg?.token;
|
|
4349
|
+
if (!token) {
|
|
4350
|
+
writeStderr("[agendex] no local token found in config — is the server running?");
|
|
4351
|
+
return 1;
|
|
4352
|
+
}
|
|
4353
|
+
const port = process.env.PORT ?? "4890";
|
|
4354
|
+
const { request } = await import("node:http");
|
|
4355
|
+
const body = JSON.stringify({ path: resolved });
|
|
4356
|
+
try {
|
|
4357
|
+
const res = await new Promise((resolve7, reject) => {
|
|
4358
|
+
const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
|
|
4359
|
+
method: "POST",
|
|
4360
|
+
headers: {
|
|
4361
|
+
Authorization: `Bearer ${token}`,
|
|
4362
|
+
"Content-Type": "application/json",
|
|
4363
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
4364
|
+
}
|
|
4365
|
+
}, (res2) => {
|
|
4366
|
+
let data = "";
|
|
4367
|
+
res2.setEncoding("utf8");
|
|
4368
|
+
res2.on("data", (chunk) => {
|
|
4369
|
+
data += chunk;
|
|
4370
|
+
});
|
|
4371
|
+
res2.on("end", () => resolve7({ status: res2.statusCode ?? 0, body: data }));
|
|
4372
|
+
res2.on("error", reject);
|
|
4373
|
+
});
|
|
4374
|
+
req.on("error", reject);
|
|
4375
|
+
req.write(body);
|
|
4376
|
+
req.end();
|
|
4377
|
+
});
|
|
4378
|
+
if (res.status >= 200 && res.status < 300) {
|
|
4379
|
+
writeStdout(`[agendex] added custom plan dir: ${resolved}`);
|
|
4380
|
+
writeStdout(`[agendex] server notified — scanning + watching now`);
|
|
4381
|
+
} else {
|
|
4382
|
+
writeStderr(`[agendex] server returned ${res.status}: ${res.body}`);
|
|
4383
|
+
return 1;
|
|
4384
|
+
}
|
|
4385
|
+
} catch (err) {
|
|
4386
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4387
|
+
writeStderr(`[agendex] could not reach local server on port ${port}: ${msg}`);
|
|
4388
|
+
return 1;
|
|
4389
|
+
}
|
|
4390
|
+
} else {
|
|
4391
|
+
const cfg = loadConfig();
|
|
4392
|
+
const currentDirs = cfg?.customPlanDirs ?? [];
|
|
4393
|
+
const updated = normalizeCustomPlanDirs([...currentDirs, resolved]);
|
|
4394
|
+
saveConfig({
|
|
4395
|
+
...cfg ?? { configVersion: 3, enabledAdapters: [] },
|
|
4396
|
+
customPlanDirs: updated
|
|
4397
|
+
});
|
|
4398
|
+
writeStdout(`[agendex] added custom plan dir: ${resolved}`);
|
|
4399
|
+
writeStdout(`[agendex] daemon will pick up the change automatically`);
|
|
4400
|
+
}
|
|
3957
4401
|
return 0;
|
|
3958
4402
|
}
|
|
3959
4403
|
case "remove-dir": {
|
|
@@ -4070,6 +4514,7 @@ Usage:
|
|
|
4070
4514
|
agendex logout Clear stored cloud token
|
|
4071
4515
|
agendex configure Select which agents/adapters to index
|
|
4072
4516
|
agendex add-dir <path> Add a custom directory to scan for plans
|
|
4517
|
+
agendex add-dir <path> --live Add dir and notify running server immediately
|
|
4073
4518
|
agendex remove-dir <path> Remove a custom directory
|
|
4074
4519
|
agendex list-dirs List custom plan directories
|
|
4075
4520
|
agendex sync One-shot scan + sync to cloud (skips unchanged plans)
|