agendex-cli 1.1.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 +47 -0
- package/dist/cli.js +518 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ 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
|
|
27
|
+
agendex upload <path> # Upload a single Markdown plan file to the cloud
|
|
28
|
+
agendex upload <path> --agent <name> # Override the uploaded plan's agent label
|
|
29
|
+
agendex upload <path> --open # Open the uploaded plan in the browser after upload
|
|
26
30
|
agendex cleanup # Interactively remove cloud daemons
|
|
27
31
|
agendex cleanup --stale # Auto-remove all stale daemons
|
|
28
32
|
agendex status # Show config state, daemon status, uptime & hostname
|
|
@@ -57,6 +61,27 @@ AGENDEX_DEV=1 agendex sync
|
|
|
57
61
|
|
|
58
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.
|
|
59
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
|
+
|
|
60
85
|
## Sync Provenance
|
|
61
86
|
|
|
62
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.
|
|
@@ -67,6 +92,28 @@ You can disable local IP address collection from Account settings in the cloud a
|
|
|
67
92
|
AGENDEX_DISABLE_LOCAL_IP=1 agendex sync
|
|
68
93
|
```
|
|
69
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
|
+
|
|
70
117
|
## Daemon Cleanup
|
|
71
118
|
|
|
72
119
|
`agendex cleanup` manages registered daemon devices in the cloud.
|
package/dist/cli.js
CHANGED
|
@@ -1059,8 +1059,8 @@ var init_cleanup = __esm(() => {
|
|
|
1059
1059
|
|
|
1060
1060
|
// src/cli.ts
|
|
1061
1061
|
import { spawn as spawn4 } from "node:child_process";
|
|
1062
|
-
import { existsSync as
|
|
1063
|
-
import { resolve as
|
|
1062
|
+
import { existsSync as existsSync13, statSync as statSync2, writeSync } from "node:fs";
|
|
1063
|
+
import { resolve as resolve10 } from "node:path";
|
|
1064
1064
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1065
1065
|
|
|
1066
1066
|
// ../shared/src/adapters/catalog.ts
|
|
@@ -2822,6 +2822,7 @@ function normalizeStoredConfig(raw) {
|
|
|
2822
2822
|
const token = typeof raw.token === "string" && raw.token.trim() ? raw.token : undefined;
|
|
2823
2823
|
const cloudToken = typeof raw.cloudToken === "string" && raw.cloudToken.trim() ? raw.cloudToken : undefined;
|
|
2824
2824
|
const convexUrl = typeof raw.convexUrl === "string" && raw.convexUrl.trim() ? raw.convexUrl : undefined;
|
|
2825
|
+
const siteUrl = typeof raw.siteUrl === "string" && raw.siteUrl.trim() ? raw.siteUrl : undefined;
|
|
2825
2826
|
const deviceId = typeof raw.deviceId === "string" && raw.deviceId.trim() ? raw.deviceId : undefined;
|
|
2826
2827
|
const collectLocalIpAddress = typeof raw.collectLocalIpAddress === "boolean" ? raw.collectLocalIpAddress : undefined;
|
|
2827
2828
|
return {
|
|
@@ -2829,6 +2830,7 @@ function normalizeStoredConfig(raw) {
|
|
|
2829
2830
|
token,
|
|
2830
2831
|
cloudToken,
|
|
2831
2832
|
convexUrl,
|
|
2833
|
+
siteUrl,
|
|
2832
2834
|
deviceId,
|
|
2833
2835
|
collectLocalIpAddress,
|
|
2834
2836
|
enabledAdapters: normalizeAdapterIds(raw.enabledAdapters),
|
|
@@ -2845,6 +2847,7 @@ function saveConfig(config) {
|
|
|
2845
2847
|
token: config.token,
|
|
2846
2848
|
cloudToken: config.cloudToken,
|
|
2847
2849
|
convexUrl: config.convexUrl,
|
|
2850
|
+
siteUrl: config.siteUrl,
|
|
2848
2851
|
deviceId: config.deviceId,
|
|
2849
2852
|
collectLocalIpAddress: config.collectLocalIpAddress,
|
|
2850
2853
|
enabledAdapters: sanitizeEnabledAdapterIds(config.enabledAdapters),
|
|
@@ -2923,6 +2926,7 @@ async function loadOrInitConfig(options = {}) {
|
|
|
2923
2926
|
token: tokenFromEnv ? existing?.token : currentToken,
|
|
2924
2927
|
cloudToken: existing?.cloudToken,
|
|
2925
2928
|
convexUrl: existing?.convexUrl,
|
|
2929
|
+
siteUrl: existing?.siteUrl,
|
|
2926
2930
|
deviceId,
|
|
2927
2931
|
enabledAdapters,
|
|
2928
2932
|
customPlanDirs: existing?.customPlanDirs ?? []
|
|
@@ -2954,6 +2958,7 @@ function isIndexablePlan(plan) {
|
|
|
2954
2958
|
}
|
|
2955
2959
|
var PROPOSED_PLAN_TAG_REGEX2 = /<\s*\/?\s*proposed_plan\s*>/gi;
|
|
2956
2960
|
var ESCAPED_PROPOSED_PLAN_TAG_REGEX2 = /<\s*\/?\s*proposed_plan\s*>/gi;
|
|
2961
|
+
var FENCED_CODE_BLOCK_REGEX = /(?:```|~~~)[\s\S]*?(?:```|~~~)/g;
|
|
2957
2962
|
var VISIBLE_TEXT_REGEX = /[\p{L}\p{N}]/u;
|
|
2958
2963
|
var LOW_VALUE_METADATA_KEYS = ["lowValue", "lowValueReasons", "lowValueSignals"];
|
|
2959
2964
|
function normalizeLineEndings2(text) {
|
|
@@ -2973,7 +2978,7 @@ function normalizePlanContent(content) {
|
|
|
2973
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();
|
|
2974
2979
|
}
|
|
2975
2980
|
function withoutLowValueMetadata(metadata) {
|
|
2976
|
-
const next = { ...metadata
|
|
2981
|
+
const next = { ...metadata };
|
|
2977
2982
|
for (const key of LOW_VALUE_METADATA_KEYS)
|
|
2978
2983
|
delete next[key];
|
|
2979
2984
|
return next;
|
|
@@ -2984,6 +2989,23 @@ function unique(items) {
|
|
|
2984
2989
|
function visibleText(text) {
|
|
2985
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();
|
|
2986
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
|
+
}
|
|
2987
3009
|
function isSeparatorLine(line) {
|
|
2988
3010
|
return /^(?:-{3,}|_{3,}|\*{3,})$/.test(line.trim());
|
|
2989
3011
|
}
|
|
@@ -3007,7 +3029,7 @@ function isOrderedListLine(line) {
|
|
|
3007
3029
|
return /^\d+[.)]\s+\S/.test(line.trim());
|
|
3008
3030
|
}
|
|
3009
3031
|
function isHeadingOnly(lines) {
|
|
3010
|
-
return lines.
|
|
3032
|
+
return lines.some(isHeadingLine) && lines.every(isHeadingLine);
|
|
3011
3033
|
}
|
|
3012
3034
|
function metadataHasPlanBlocks(metadata) {
|
|
3013
3035
|
const planBlocks = metadata?.planBlocks;
|
|
@@ -3108,13 +3130,20 @@ function collectPositiveSignals(normalized, lines, metadata) {
|
|
|
3108
3130
|
}).length;
|
|
3109
3131
|
if (actionBulletCount >= 2)
|
|
3110
3132
|
signals.push("action-bullets");
|
|
3111
|
-
|
|
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)) {
|
|
3112
3135
|
signals.push("future-plan-language");
|
|
3113
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
|
+
}
|
|
3114
3143
|
return unique(signals);
|
|
3115
3144
|
}
|
|
3116
3145
|
function isStrongPositiveSignal(signal) {
|
|
3117
|
-
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";
|
|
3118
3147
|
}
|
|
3119
3148
|
function hasStrongPlanSignal(positiveSignals) {
|
|
3120
3149
|
const sectionCount = positiveSignals.filter((signal) => signal.startsWith("section:")).length;
|
|
@@ -3177,6 +3206,13 @@ function looksLikeSystemContext(normalized, lines) {
|
|
|
3177
3206
|
function looksLikeToolLog(normalized) {
|
|
3178
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);
|
|
3179
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
|
+
}
|
|
3180
3216
|
function looksLikeExecutionReport(normalized) {
|
|
3181
3217
|
const hasPastCompletion = /\b(?:fixed|pushed|committed|completed|done|implemented|updated|changed|patched|merged|deployed|passed|failed|resolved|reverted)\b/i.test(normalized);
|
|
3182
3218
|
const hasReportSection = /^\s*(?:summary|result|results|changes|verification|status)\s*:/im.test(normalized);
|
|
@@ -3208,14 +3244,20 @@ function assessPlanValue(input) {
|
|
|
3208
3244
|
const strongPositive = hasStrongPlanSignal(positiveSignals);
|
|
3209
3245
|
const systemContext = looksLikeSystemContext(normalized, lines);
|
|
3210
3246
|
const toolLog = looksLikeToolLog(normalized);
|
|
3247
|
+
const conversationArtifact = looksLikeConversationArtifact(lines);
|
|
3211
3248
|
const executionReport = looksLikeExecutionReport(normalized);
|
|
3212
3249
|
const wrapperTitle = looksLikeWrapperTitle(input.title);
|
|
3213
3250
|
const promptTitle = looksLikePromptTitle(input.title);
|
|
3214
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;
|
|
3215
3255
|
if (systemContext)
|
|
3216
3256
|
signals.push("negative:system-context");
|
|
3217
3257
|
if (toolLog)
|
|
3218
3258
|
signals.push("negative:tool-log");
|
|
3259
|
+
if (conversationArtifact)
|
|
3260
|
+
signals.push("negative:conversation-artifact");
|
|
3219
3261
|
if (executionReport)
|
|
3220
3262
|
signals.push("negative:execution-report");
|
|
3221
3263
|
if (wrapperTitle)
|
|
@@ -3224,24 +3266,36 @@ function assessPlanValue(input) {
|
|
|
3224
3266
|
signals.push("negative:prompt-title");
|
|
3225
3267
|
if (reviewOutput)
|
|
3226
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");
|
|
3227
3275
|
if (lines.length === 1)
|
|
3228
3276
|
signals.push("shape:single-line");
|
|
3229
3277
|
if (lines.length === 1 && positiveSignals.length === 0) {
|
|
3230
3278
|
reasons.push(isPromptLikeOneLiner(lines[0] ?? "") ? "prompt-like" : "no-plan-signals");
|
|
3231
3279
|
}
|
|
3232
|
-
if (systemContext && !
|
|
3280
|
+
if (systemContext && !explicitPlanBlock)
|
|
3233
3281
|
reasons.push("system-context");
|
|
3234
|
-
if (
|
|
3282
|
+
if (toolLog && !explicitPlanBlock)
|
|
3283
|
+
reasons.push("tool-log");
|
|
3284
|
+
if (conversationArtifact && !explicitPlanBlock && !strongPositive)
|
|
3285
|
+
reasons.push("conversation-artifact");
|
|
3286
|
+
if (executionReport && !explicitPlanBlock)
|
|
3235
3287
|
reasons.push("execution-report");
|
|
3236
3288
|
if (wrapperTitle && !explicitPlanBlock)
|
|
3237
3289
|
reasons.push("wrapper-title");
|
|
3238
3290
|
if (reviewOutput && !explicitPlanBlock)
|
|
3239
3291
|
reasons.push("review-output");
|
|
3240
|
-
if (promptTitle && !strongPositive &&
|
|
3292
|
+
if (promptTitle && !strongPositive && !explicitPlanBlock)
|
|
3241
3293
|
reasons.push("prompt-like");
|
|
3242
|
-
if (
|
|
3243
|
-
reasons.push("
|
|
3244
|
-
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")) {
|
|
3245
3299
|
reasons.push("no-plan-signals");
|
|
3246
3300
|
}
|
|
3247
3301
|
return lowValueAssessment(reasons, signals);
|
|
@@ -3422,6 +3476,9 @@ async function walkDir(dir, depth = 0, seen = new Set) {
|
|
|
3422
3476
|
} catch {}
|
|
3423
3477
|
return files;
|
|
3424
3478
|
}
|
|
3479
|
+
function preparePlanForIndex(plan) {
|
|
3480
|
+
return annotatePlanValueMetadata(plan);
|
|
3481
|
+
}
|
|
3425
3482
|
async function parseGenericMarkdownPlan(filePath, extraMetadata) {
|
|
3426
3483
|
try {
|
|
3427
3484
|
const content = await readFile7(filePath, "utf-8");
|
|
@@ -3461,7 +3518,7 @@ async function scanUserPlans(into) {
|
|
|
3461
3518
|
continue;
|
|
3462
3519
|
const plan = await parseGenericMarkdownPlan(file, { userCreated: true });
|
|
3463
3520
|
if (plan)
|
|
3464
|
-
into.set(plan.id, plan);
|
|
3521
|
+
into.set(plan.id, preparePlanForIndex(plan));
|
|
3465
3522
|
}
|
|
3466
3523
|
}
|
|
3467
3524
|
function getCustomPlanDirs() {
|
|
@@ -3523,7 +3580,7 @@ async function scanCustomPlanDirs(coveredPaths, into) {
|
|
|
3523
3580
|
agentHint: dirBasename
|
|
3524
3581
|
});
|
|
3525
3582
|
if (plan) {
|
|
3526
|
-
into.set(plan.id, plan);
|
|
3583
|
+
into.set(plan.id, preparePlanForIndex(plan));
|
|
3527
3584
|
count++;
|
|
3528
3585
|
}
|
|
3529
3586
|
}
|
|
@@ -3543,7 +3600,7 @@ async function scan() {
|
|
|
3543
3600
|
continue;
|
|
3544
3601
|
const plans = await adapter.parse(file);
|
|
3545
3602
|
for (const plan of plans) {
|
|
3546
|
-
const annotated =
|
|
3603
|
+
const annotated = preparePlanForIndex(plan);
|
|
3547
3604
|
next.set(annotated.id, annotated);
|
|
3548
3605
|
}
|
|
3549
3606
|
}
|
|
@@ -3563,7 +3620,7 @@ async function scan() {
|
|
|
3563
3620
|
continue;
|
|
3564
3621
|
const plans = await adapter.parse(file);
|
|
3565
3622
|
for (const plan of plans) {
|
|
3566
|
-
const annotated =
|
|
3623
|
+
const annotated = preparePlanForIndex(plan);
|
|
3567
3624
|
next.set(annotated.id, annotated);
|
|
3568
3625
|
}
|
|
3569
3626
|
}
|
|
@@ -3660,7 +3717,7 @@ async function rescanFile(filePath) {
|
|
|
3660
3717
|
removedPlans.push(...removePlansForPath(filePath, adapter));
|
|
3661
3718
|
continue;
|
|
3662
3719
|
}
|
|
3663
|
-
const plans = rawPlans.map(
|
|
3720
|
+
const plans = rawPlans.map(preparePlanForIndex);
|
|
3664
3721
|
for (const plan of plans) {
|
|
3665
3722
|
store.set(plan.id, plan);
|
|
3666
3723
|
}
|
|
@@ -3673,9 +3730,10 @@ async function rescanFile(filePath) {
|
|
|
3673
3730
|
if (normalized.endsWith(".md") && (normalized.startsWith(userPlansDir + sep3) || normalized === userPlansDir)) {
|
|
3674
3731
|
const plan = await parseGenericMarkdownPlan(filePath, { userCreated: true });
|
|
3675
3732
|
if (plan) {
|
|
3676
|
-
|
|
3733
|
+
const annotated = preparePlanForIndex(plan);
|
|
3734
|
+
store.set(annotated.id, annotated);
|
|
3677
3735
|
notifyPlansChanged();
|
|
3678
|
-
return [
|
|
3736
|
+
return [annotated];
|
|
3679
3737
|
}
|
|
3680
3738
|
}
|
|
3681
3739
|
if (normalized.endsWith(".md")) {
|
|
@@ -3688,9 +3746,10 @@ async function rescanFile(filePath) {
|
|
|
3688
3746
|
customDir: dir
|
|
3689
3747
|
});
|
|
3690
3748
|
if (plan) {
|
|
3691
|
-
|
|
3749
|
+
const annotated = preparePlanForIndex(plan);
|
|
3750
|
+
store.set(annotated.id, annotated);
|
|
3692
3751
|
notifyPlansChanged();
|
|
3693
|
-
return [
|
|
3752
|
+
return [annotated];
|
|
3694
3753
|
}
|
|
3695
3754
|
}
|
|
3696
3755
|
}
|
|
@@ -3751,6 +3810,38 @@ function startWatching(onChange) {
|
|
|
3751
3810
|
function stopWatching() {
|
|
3752
3811
|
closeAllWatchers();
|
|
3753
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
|
+
}
|
|
3754
3845
|
function setupWatchers(onChange) {
|
|
3755
3846
|
const adapters = getActiveAdapters();
|
|
3756
3847
|
const watchedPaths = new Set;
|
|
@@ -3870,7 +3961,8 @@ function parseSyncSuccess(body) {
|
|
|
3870
3961
|
return {
|
|
3871
3962
|
ok: true,
|
|
3872
3963
|
skippedLowValue: result.skippedLowValue === true,
|
|
3873
|
-
deleted: result.deleted === true
|
|
3964
|
+
deleted: result.deleted === true,
|
|
3965
|
+
...typeof result.planId === "string" && { planId: result.planId }
|
|
3874
3966
|
};
|
|
3875
3967
|
} catch {
|
|
3876
3968
|
return { ok: true };
|
|
@@ -3905,7 +3997,7 @@ async function syncPlan(plan) {
|
|
|
3905
3997
|
}
|
|
3906
3998
|
}
|
|
3907
3999
|
if (res.status < 200 || res.status >= 300) {
|
|
3908
|
-
return { ok: false, error: `${res.status}: ${res.body}` };
|
|
4000
|
+
return { ok: false, status: res.status, error: `${res.status}: ${res.body}` };
|
|
3909
4001
|
}
|
|
3910
4002
|
return parseSyncSuccess(res.body);
|
|
3911
4003
|
}
|
|
@@ -4191,6 +4283,12 @@ function getDefaultSiteUrl() {
|
|
|
4191
4283
|
return process.env.AGENDEX_SITE_URL;
|
|
4192
4284
|
return isDevMode() ? DEV_SITE_URL : PROD_SITE_URL;
|
|
4193
4285
|
}
|
|
4286
|
+
function getSiteUrl() {
|
|
4287
|
+
const config = loadConfig();
|
|
4288
|
+
if (config?.siteUrl)
|
|
4289
|
+
return config.siteUrl;
|
|
4290
|
+
return getDefaultSiteUrl();
|
|
4291
|
+
}
|
|
4194
4292
|
function launchBrowser(url, label) {
|
|
4195
4293
|
console.log(`[agendex] Opening ${label}...`);
|
|
4196
4294
|
console.log(`[agendex] If it doesn't open, visit: ${url}`);
|
|
@@ -4203,15 +4301,16 @@ function launchBrowser(url, label) {
|
|
|
4203
4301
|
async function login(siteUrlOverride) {
|
|
4204
4302
|
const { port, result } = await startCallbackServer();
|
|
4205
4303
|
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
4206
|
-
const
|
|
4304
|
+
const existing = loadConfig();
|
|
4305
|
+
const siteUrl = siteUrlOverride ?? existing?.siteUrl ?? getDefaultSiteUrl();
|
|
4207
4306
|
const authUrl = `${siteUrl}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
|
|
4208
4307
|
launchBrowser(authUrl, "browser for authentication");
|
|
4209
4308
|
const callback = await result;
|
|
4210
|
-
const existing = loadConfig();
|
|
4211
4309
|
const config = {
|
|
4212
4310
|
configVersion: 3,
|
|
4213
4311
|
cloudToken: callback.token,
|
|
4214
4312
|
convexUrl: callback.convexUrl,
|
|
4313
|
+
siteUrl,
|
|
4215
4314
|
enabledAdapters: existing?.enabledAdapters ?? [],
|
|
4216
4315
|
customPlanDirs: existing?.customPlanDirs ?? [],
|
|
4217
4316
|
...existing?.token ? { token: existing.token } : {},
|
|
@@ -4232,7 +4331,8 @@ function logout() {
|
|
|
4232
4331
|
enabledAdapters: existing.enabledAdapters,
|
|
4233
4332
|
customPlanDirs: existing.customPlanDirs,
|
|
4234
4333
|
...existing.token ? { token: existing.token } : {},
|
|
4235
|
-
...existing.deviceId ? { deviceId: existing.deviceId } : {}
|
|
4334
|
+
...existing.deviceId ? { deviceId: existing.deviceId } : {},
|
|
4335
|
+
...existing.siteUrl ? { siteUrl: existing.siteUrl } : {}
|
|
4236
4336
|
};
|
|
4237
4337
|
saveConfig(config);
|
|
4238
4338
|
console.log("[agendex] Logged out. Cloud token removed.");
|
|
@@ -4376,7 +4476,7 @@ function spawnBrowser(command, args, options = {}) {
|
|
|
4376
4476
|
// src/daemon.ts
|
|
4377
4477
|
import { spawn as spawn2 } from "node:child_process";
|
|
4378
4478
|
import { hostname as osHostname2 } from "node:os";
|
|
4379
|
-
import { resolve as
|
|
4479
|
+
import { resolve as resolve7 } from "node:path";
|
|
4380
4480
|
import { fileURLToPath } from "node:url";
|
|
4381
4481
|
|
|
4382
4482
|
// src/adapters.ts
|
|
@@ -4405,6 +4505,86 @@ function resolveCliAdapterIds(config) {
|
|
|
4405
4505
|
return ids;
|
|
4406
4506
|
}
|
|
4407
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
|
+
|
|
4408
4588
|
// src/network.ts
|
|
4409
4589
|
import { execFileSync } from "node:child_process";
|
|
4410
4590
|
import { networkInterfaces } from "node:os";
|
|
@@ -4521,6 +4701,7 @@ function isIpv4Address(address) {
|
|
|
4521
4701
|
}
|
|
4522
4702
|
|
|
4523
4703
|
// src/payload.ts
|
|
4704
|
+
import { basename as basename6, resolve as resolve6 } from "node:path";
|
|
4524
4705
|
var SYNC_METADATA_KEY = "agendexSync";
|
|
4525
4706
|
function isRecord4(value) {
|
|
4526
4707
|
return typeof value === "object" && value !== null;
|
|
@@ -4539,6 +4720,36 @@ function withSyncDeviceMetadata(metadata, deviceId, hostname2, ipAddress) {
|
|
|
4539
4720
|
}
|
|
4540
4721
|
};
|
|
4541
4722
|
}
|
|
4723
|
+
function parseUploadFile(filePath, content, agentOverride) {
|
|
4724
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
4725
|
+
const override = agentOverride?.trim();
|
|
4726
|
+
let agent = override || "uploaded";
|
|
4727
|
+
if (!override && fmMatch) {
|
|
4728
|
+
const agentLine = fmMatch[1]?.match(/^agent:\s*(.+)$/m);
|
|
4729
|
+
if (agentLine?.[1])
|
|
4730
|
+
agent = agentLine[1].trim();
|
|
4731
|
+
}
|
|
4732
|
+
const body = fmMatch ? content.slice(fmMatch[0].length) : content;
|
|
4733
|
+
const titleMatch = body.match(/^#\s+(.+)/m);
|
|
4734
|
+
const title = titleMatch?.[1]?.trim() || basename6(filePath).replace(/\.md$/i, "") || "Untitled";
|
|
4735
|
+
return { title, agent, body };
|
|
4736
|
+
}
|
|
4737
|
+
function fileToSyncPayload(filePath, content, options = {}) {
|
|
4738
|
+
const absolutePath = resolve6(filePath);
|
|
4739
|
+
const { title, agent, body } = parseUploadFile(absolutePath, content, options.agentOverride);
|
|
4740
|
+
const now = Date.now();
|
|
4741
|
+
return {
|
|
4742
|
+
localPlanId: hashPath(absolutePath),
|
|
4743
|
+
agent,
|
|
4744
|
+
title,
|
|
4745
|
+
content: body,
|
|
4746
|
+
format: "md",
|
|
4747
|
+
filePath: absolutePath,
|
|
4748
|
+
metadata: withSyncDeviceMetadata({ uploaded: true, userCreated: true }, options.deviceId, options.hostname, options.ipAddress),
|
|
4749
|
+
createdAt: options.createdAt ?? now,
|
|
4750
|
+
updatedAt: options.updatedAt ?? now
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4542
4753
|
function planToSyncPayload(plan, deviceId, hostname2, ipAddress) {
|
|
4543
4754
|
return {
|
|
4544
4755
|
localPlanId: plan.id,
|
|
@@ -4554,54 +4765,6 @@ function planToSyncPayload(plan, deviceId, hostname2, ipAddress) {
|
|
|
4554
4765
|
};
|
|
4555
4766
|
}
|
|
4556
4767
|
|
|
4557
|
-
// src/sync-cache.ts
|
|
4558
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4559
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4560
|
-
import { join as join12 } from "node:path";
|
|
4561
|
-
function getCachePath() {
|
|
4562
|
-
return join12(getConfigDir(), "sync-cache.json");
|
|
4563
|
-
}
|
|
4564
|
-
function loadSyncCache() {
|
|
4565
|
-
const cachePath = getCachePath();
|
|
4566
|
-
if (!existsSync8(cachePath))
|
|
4567
|
-
return {};
|
|
4568
|
-
try {
|
|
4569
|
-
const raw = JSON.parse(readFileSync5(cachePath, "utf-8"));
|
|
4570
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
4571
|
-
return {};
|
|
4572
|
-
return raw;
|
|
4573
|
-
} catch {
|
|
4574
|
-
return {};
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4577
|
-
function saveSyncCache(cache, options) {
|
|
4578
|
-
const dir = getConfigDir();
|
|
4579
|
-
if (!existsSync8(dir))
|
|
4580
|
-
mkdirSync3(dir, { recursive: true });
|
|
4581
|
-
const cachePath = getCachePath();
|
|
4582
|
-
if (options?.replace) {
|
|
4583
|
-
writeFileSync3(cachePath, JSON.stringify(cache));
|
|
4584
|
-
return;
|
|
4585
|
-
}
|
|
4586
|
-
const existing = loadSyncCache();
|
|
4587
|
-
writeFileSync3(cachePath, JSON.stringify({ ...existing, ...cache }));
|
|
4588
|
-
}
|
|
4589
|
-
function computePayloadHash(payload) {
|
|
4590
|
-
const canonical = JSON.stringify([
|
|
4591
|
-
payload.localPlanId,
|
|
4592
|
-
payload.agent,
|
|
4593
|
-
payload.title,
|
|
4594
|
-
payload.content,
|
|
4595
|
-
payload.format,
|
|
4596
|
-
payload.filePath ?? null,
|
|
4597
|
-
payload.workspace ?? null,
|
|
4598
|
-
payload.metadata ?? null,
|
|
4599
|
-
payload.createdAt ?? null,
|
|
4600
|
-
payload.updatedAt ?? null
|
|
4601
|
-
]);
|
|
4602
|
-
return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
|
|
4603
|
-
}
|
|
4604
|
-
|
|
4605
4768
|
// src/sync-privacy.ts
|
|
4606
4769
|
async function shouldIncludeLocalIpAddressInSync() {
|
|
4607
4770
|
if (!shouldCollectLocalIpAddress())
|
|
@@ -4668,6 +4831,10 @@ var PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS = 15000;
|
|
|
4668
4831
|
var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
|
|
4669
4832
|
var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the write-back payload.";
|
|
4670
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;
|
|
4671
4838
|
function isRecord5(value) {
|
|
4672
4839
|
return typeof value === "object" && value !== null;
|
|
4673
4840
|
}
|
|
@@ -4701,6 +4868,9 @@ async function runWorker() {
|
|
|
4701
4868
|
setActiveAdapters(adapters);
|
|
4702
4869
|
const syncCache = loadSyncCache();
|
|
4703
4870
|
const syncQueue = [];
|
|
4871
|
+
const retryQueue = [];
|
|
4872
|
+
const retryAttemptByPlanId = new Map;
|
|
4873
|
+
const lastSyncedUpdatedAt = new Map;
|
|
4704
4874
|
const pendingWritebackReports = loadPendingWritebackReports();
|
|
4705
4875
|
const liveSessions = new Map;
|
|
4706
4876
|
let syncing = false;
|
|
@@ -4726,17 +4896,76 @@ async function runWorker() {
|
|
|
4726
4896
|
console.log("[agendex] cloud token refreshed");
|
|
4727
4897
|
return true;
|
|
4728
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
|
+
}
|
|
4729
4952
|
async function processSyncQueue() {
|
|
4730
4953
|
if (syncing || syncQueue.length === 0)
|
|
4731
4954
|
return;
|
|
4732
4955
|
syncing = true;
|
|
4733
|
-
const batch = syncQueue.splice(0);
|
|
4956
|
+
const batch = dedupeSyncPayloads(syncQueue.splice(0));
|
|
4734
4957
|
let syncedCount = 0;
|
|
4735
4958
|
let lowValueSkippedCount = 0;
|
|
4736
4959
|
let lowValueDeletedCount = 0;
|
|
4737
4960
|
let failedCount = 0;
|
|
4961
|
+
const failedPayloads = [];
|
|
4738
4962
|
try {
|
|
4739
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
|
+
}
|
|
4740
4969
|
let result = await syncPlan(payload);
|
|
4741
4970
|
if (!result.ok && result.error?.includes("401")) {
|
|
4742
4971
|
const refreshed = await tryRefreshToken();
|
|
@@ -4747,11 +4976,12 @@ async function runWorker() {
|
|
|
4747
4976
|
if (!result.ok) {
|
|
4748
4977
|
if (result.error?.includes("401")) {
|
|
4749
4978
|
console.error("[agendex] session expired. Run `agendex login` to re-authenticate.");
|
|
4750
|
-
batch.length = 0;
|
|
4751
4979
|
syncQueue.length = 0;
|
|
4980
|
+
retryQueue.length = 0;
|
|
4752
4981
|
break;
|
|
4753
4982
|
}
|
|
4754
4983
|
failedCount++;
|
|
4984
|
+
failedPayloads.push(payload);
|
|
4755
4985
|
console.error(`[agendex] sync failed for "${payload.title}": ${result.error}`);
|
|
4756
4986
|
} else {
|
|
4757
4987
|
if (result.skippedLowValue) {
|
|
@@ -4762,14 +4992,23 @@ async function runWorker() {
|
|
|
4762
4992
|
syncedCount++;
|
|
4763
4993
|
}
|
|
4764
4994
|
syncCache[payload.localPlanId] = computePayloadHash(payload);
|
|
4995
|
+
retryAttemptByPlanId.delete(payload.localPlanId);
|
|
4996
|
+
if (payload.updatedAt !== undefined) {
|
|
4997
|
+
lastSyncedUpdatedAt.set(payload.localPlanId, payload.updatedAt);
|
|
4998
|
+
}
|
|
4765
4999
|
}
|
|
4766
5000
|
}
|
|
4767
5001
|
} catch (err) {
|
|
4768
5002
|
console.error("[agendex] sync error:", err);
|
|
4769
|
-
|
|
5003
|
+
pushToSyncQueue(...batch.slice(syncedCount + lowValueSkippedCount + failedCount));
|
|
4770
5004
|
} finally {
|
|
4771
5005
|
syncing = false;
|
|
4772
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
|
+
}
|
|
4773
5012
|
if (syncedCount > 0 || lowValueSkippedCount > 0 || failedCount > 0) {
|
|
4774
5013
|
saveSyncCache(syncCache);
|
|
4775
5014
|
const lowValueSuffix2 = lowValueSkippedCount > 0 ? `, ${lowValueSkippedCount} low-value skipped/pruned${lowValueDeletedCount > 0 ? ` (${lowValueDeletedCount} deleted)` : ""}` : "";
|
|
@@ -4791,7 +5030,7 @@ async function runWorker() {
|
|
|
4791
5030
|
if (livePayloads.has(planId))
|
|
4792
5031
|
continue;
|
|
4793
5032
|
const endedPayload = buildEndedPlannotatorPayload(lastPayload);
|
|
4794
|
-
|
|
5033
|
+
pushToSyncQueue(endedPayload);
|
|
4795
5034
|
liveSessions.delete(planId);
|
|
4796
5035
|
queued = true;
|
|
4797
5036
|
console.log(`[agendex] Plannotator session ended: ${endedPayload.title}`);
|
|
@@ -4857,7 +5096,7 @@ async function runWorker() {
|
|
|
4857
5096
|
const updatedPlan = getById(job.localPlanId);
|
|
4858
5097
|
if (updatedPlan) {
|
|
4859
5098
|
const updatedPayload = planToSyncPayload(updatedPlan, config.deviceId, hostname2, await getSyncIpAddress());
|
|
4860
|
-
|
|
5099
|
+
pushToSyncQueue(updatedPayload);
|
|
4861
5100
|
if (isLivePlannotatorPayload(updatedPayload)) {
|
|
4862
5101
|
liveSessions.set(updatedPayload.localPlanId, updatedPayload);
|
|
4863
5102
|
}
|
|
@@ -4892,7 +5131,15 @@ async function runWorker() {
|
|
|
4892
5131
|
pollingWritebacks = false;
|
|
4893
5132
|
}
|
|
4894
5133
|
}
|
|
4895
|
-
|
|
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
|
+
};
|
|
4896
5143
|
console.log(`[agendex] initial scan...`);
|
|
4897
5144
|
await scan();
|
|
4898
5145
|
const plans = getAll();
|
|
@@ -4904,8 +5151,7 @@ async function runWorker() {
|
|
|
4904
5151
|
const initialIpAddress = await getSyncIpAddress();
|
|
4905
5152
|
for (const plan of plans) {
|
|
4906
5153
|
const payload = planToSyncPayload(plan, config.deviceId, hostname2, initialIpAddress);
|
|
4907
|
-
|
|
4908
|
-
if (syncCache[plan.id] === hash) {
|
|
5154
|
+
if (syncCache[plan.id] === computePayloadHash(payload)) {
|
|
4909
5155
|
initialSkipped++;
|
|
4910
5156
|
continue;
|
|
4911
5157
|
}
|
|
@@ -4914,7 +5160,7 @@ async function runWorker() {
|
|
|
4914
5160
|
} else {
|
|
4915
5161
|
initialQueuedSyncable++;
|
|
4916
5162
|
}
|
|
4917
|
-
|
|
5163
|
+
pushToSyncQueue(payload);
|
|
4918
5164
|
}
|
|
4919
5165
|
const activePlanIds = new Set(plans.map((plan) => plan.id));
|
|
4920
5166
|
for (const id of Object.keys(syncCache)) {
|
|
@@ -4926,35 +5172,75 @@ async function runWorker() {
|
|
|
4926
5172
|
console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
|
|
4927
5173
|
await processSyncQueue();
|
|
4928
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
|
+
});
|
|
4929
5180
|
setInterval(() => {
|
|
4930
5181
|
(async () => {
|
|
4931
5182
|
await sendHeartbeat(await getSyncIpAddress());
|
|
4932
5183
|
})().catch(() => {});
|
|
4933
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
|
+
}
|
|
4934
5209
|
if (shouldEnablePlannotatorSync(config)) {
|
|
4935
5210
|
setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
|
|
4936
5211
|
pollPlannotatorWritebacks();
|
|
4937
5212
|
setInterval(() => {
|
|
4938
5213
|
(async () => {
|
|
4939
5214
|
await scan();
|
|
5215
|
+
await enqueueChangedPlans(getAll());
|
|
4940
5216
|
await reconcileLivePlannotatorSessions(getAll());
|
|
4941
5217
|
})().catch((err) => {
|
|
4942
5218
|
console.error("[agendex] Plannotator liveness sweep failed:", err);
|
|
4943
5219
|
});
|
|
4944
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
|
+
}
|
|
4945
5242
|
}
|
|
4946
|
-
startWatching(
|
|
4947
|
-
(async () => {
|
|
4948
|
-
const ipAddress = await getSyncIpAddress();
|
|
4949
|
-
for (const plan of changedPlans) {
|
|
4950
|
-
syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
|
|
4951
|
-
}
|
|
4952
|
-
processSyncQueue();
|
|
4953
|
-
await reconcileLivePlannotatorSessions(getAll());
|
|
4954
|
-
})().catch((err) => {
|
|
4955
|
-
console.error("[agendex] failed to queue changed plans:", err);
|
|
4956
|
-
});
|
|
4957
|
-
});
|
|
5243
|
+
startWatching(onPlansFileChange);
|
|
4958
5244
|
console.log(`[agendex] daemon running. Watching for file changes...`);
|
|
4959
5245
|
async function gracefulShutdown() {
|
|
4960
5246
|
stopWatching();
|
|
@@ -4981,7 +5267,7 @@ async function startSupervisor() {
|
|
|
4981
5267
|
};
|
|
4982
5268
|
process.on("SIGTERM", shutdown);
|
|
4983
5269
|
process.on("SIGINT", shutdown);
|
|
4984
|
-
const scriptPath =
|
|
5270
|
+
const scriptPath = resolve7(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
|
|
4985
5271
|
const restartTimes = [];
|
|
4986
5272
|
while (!stopping) {
|
|
4987
5273
|
const workerArgs = [scriptPath, "start", "--worker"];
|
|
@@ -4991,11 +5277,11 @@ async function startSupervisor() {
|
|
|
4991
5277
|
stdio: ["ignore", "inherit", "inherit"],
|
|
4992
5278
|
env: { ...process.env, ...isDevMode() ? { AGENDEX_DEV: "1" } : {} }
|
|
4993
5279
|
});
|
|
4994
|
-
const exitCode = await new Promise((
|
|
4995
|
-
workerProc?.once("exit", (code) =>
|
|
5280
|
+
const exitCode = await new Promise((resolve8) => {
|
|
5281
|
+
workerProc?.once("exit", (code) => resolve8(code));
|
|
4996
5282
|
workerProc?.once("error", (error) => {
|
|
4997
5283
|
console.error("[agendex] failed to spawn worker:", error);
|
|
4998
|
-
|
|
5284
|
+
resolve8(1);
|
|
4999
5285
|
});
|
|
5000
5286
|
});
|
|
5001
5287
|
workerProc = null;
|
|
@@ -5021,7 +5307,7 @@ async function startSupervisor() {
|
|
|
5021
5307
|
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
5022
5308
|
import { mkdir as mkdir2, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
5023
5309
|
import { homedir as homedir10 } from "node:os";
|
|
5024
|
-
import { dirname as dirname4, join as join14, resolve as
|
|
5310
|
+
import { dirname as dirname4, join as join14, resolve as resolve8 } from "node:path";
|
|
5025
5311
|
var SUPPORTED_AGENTS = ["claude-code", "codex", "pi"];
|
|
5026
5312
|
var MANAGED_MARKER = "agendex-plan-review";
|
|
5027
5313
|
var HOOK_TIMEOUT_SECONDS = 345600;
|
|
@@ -5033,7 +5319,7 @@ function commandFor(cliEntry, agent) {
|
|
|
5033
5319
|
return `${shellQuote(process.execPath)} ${shellQuote(cliEntry)} review-plan --hook --agent ${agent}`;
|
|
5034
5320
|
}
|
|
5035
5321
|
function scopeRoot(scope) {
|
|
5036
|
-
return scope === "repo" ?
|
|
5322
|
+
return scope === "repo" ? resolve8(process.env.PWD || process.cwd()) : homedir10();
|
|
5037
5323
|
}
|
|
5038
5324
|
function hooksJsonPath(agent, scope) {
|
|
5039
5325
|
const root = scopeRoot(scope);
|
|
@@ -5358,7 +5644,7 @@ async function runHooksCommand(args, cliEntry) {
|
|
|
5358
5644
|
for (const agent of agents) {
|
|
5359
5645
|
if (agent === "claude-code")
|
|
5360
5646
|
printClaudePreviewWarning(dryRun);
|
|
5361
|
-
const path = await installAgent(agent, scope,
|
|
5647
|
+
const path = await installAgent(agent, scope, resolve8(cliEntry), dryRun);
|
|
5362
5648
|
console.log(`[agendex] ${dryRun ? "would install" : "installed"} ${agent} hook: ${path}`);
|
|
5363
5649
|
}
|
|
5364
5650
|
return 0;
|
|
@@ -5448,7 +5734,7 @@ async function syncAll(force = false) {
|
|
|
5448
5734
|
// src/upgrade.ts
|
|
5449
5735
|
import { spawn as spawn3, spawnSync } from "node:child_process";
|
|
5450
5736
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
5451
|
-
import { dirname as dirname5, resolve as
|
|
5737
|
+
import { dirname as dirname5, resolve as resolve9, sep as sep4 } from "node:path";
|
|
5452
5738
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5453
5739
|
|
|
5454
5740
|
// src/version.ts
|
|
@@ -5458,7 +5744,7 @@ import { join as join15 } from "node:path";
|
|
|
5458
5744
|
// package.json
|
|
5459
5745
|
var package_default = {
|
|
5460
5746
|
name: "agendex-cli",
|
|
5461
|
-
version: "1.
|
|
5747
|
+
version: "1.3.0",
|
|
5462
5748
|
description: "Agendex CLI for login, sync, and daemon workflows",
|
|
5463
5749
|
homepage: "https://github.com/Tyru5/Agendex#readme",
|
|
5464
5750
|
bugs: {
|
|
@@ -5581,9 +5867,9 @@ var PACKAGE_NAME = "agendex-cli";
|
|
|
5581
5867
|
var moduleDir = dirname5(fileURLToPath2(import.meta.url));
|
|
5582
5868
|
function getPackageRoot() {
|
|
5583
5869
|
try {
|
|
5584
|
-
return realpathSync2(
|
|
5870
|
+
return realpathSync2(resolve9(moduleDir, ".."));
|
|
5585
5871
|
} catch {
|
|
5586
|
-
return
|
|
5872
|
+
return resolve9(moduleDir, "..");
|
|
5587
5873
|
}
|
|
5588
5874
|
}
|
|
5589
5875
|
function detectPackageManager(packageRoot) {
|
|
@@ -5765,9 +6051,120 @@ async function runUpgrade(opts) {
|
|
|
5765
6051
|
});
|
|
5766
6052
|
}
|
|
5767
6053
|
|
|
6054
|
+
// src/upload.ts
|
|
6055
|
+
import { readFile as readFile8, stat as stat8 } from "node:fs/promises";
|
|
6056
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
6057
|
+
import { hostname as osHostname4 } from "node:os";
|
|
6058
|
+
function resolveAgentOverride(args) {
|
|
6059
|
+
const idx = args.indexOf("--agent");
|
|
6060
|
+
if (idx === -1)
|
|
6061
|
+
return;
|
|
6062
|
+
const value = args[idx + 1];
|
|
6063
|
+
if (value === undefined || value.startsWith("--"))
|
|
6064
|
+
return "missing";
|
|
6065
|
+
return value;
|
|
6066
|
+
}
|
|
6067
|
+
function resolvePathArg(args) {
|
|
6068
|
+
for (let i = 0;i < args.length; i++) {
|
|
6069
|
+
const a = args[i];
|
|
6070
|
+
if (a === undefined)
|
|
6071
|
+
continue;
|
|
6072
|
+
if (a === "upload" || a === "--dev" || a === "--open")
|
|
6073
|
+
continue;
|
|
6074
|
+
if (a === "--agent") {
|
|
6075
|
+
i++;
|
|
6076
|
+
continue;
|
|
6077
|
+
}
|
|
6078
|
+
if (a.startsWith("--"))
|
|
6079
|
+
continue;
|
|
6080
|
+
return a;
|
|
6081
|
+
}
|
|
6082
|
+
return;
|
|
6083
|
+
}
|
|
6084
|
+
async function runUpload(args, deps) {
|
|
6085
|
+
const syncPlanFn = deps?.syncPlan ?? syncPlan;
|
|
6086
|
+
const log = deps?.log ?? ((m) => console.log(m));
|
|
6087
|
+
const error = deps?.error ?? ((m) => console.error(m));
|
|
6088
|
+
const openBrowser2 = deps?.openBrowser ?? launchBrowser;
|
|
6089
|
+
const pathArg = resolvePathArg(args);
|
|
6090
|
+
if (!pathArg || !pathArg.trim()) {
|
|
6091
|
+
error("[agendex] usage: agendex upload <path> [--agent <name>] [--open]");
|
|
6092
|
+
return 1;
|
|
6093
|
+
}
|
|
6094
|
+
const absolutePath = resolveCustomPlanDirPath(pathArg);
|
|
6095
|
+
if (!existsSync12(absolutePath)) {
|
|
6096
|
+
error(`[agendex] path does not exist: ${absolutePath}`);
|
|
6097
|
+
return 1;
|
|
6098
|
+
}
|
|
6099
|
+
let stats;
|
|
6100
|
+
try {
|
|
6101
|
+
stats = await stat8(absolutePath);
|
|
6102
|
+
} catch (err) {
|
|
6103
|
+
error(`[agendex] could not read path: ${err instanceof Error ? err.message : String(err)}`);
|
|
6104
|
+
return 1;
|
|
6105
|
+
}
|
|
6106
|
+
if (stats.isDirectory()) {
|
|
6107
|
+
error(`[agendex] path is a directory, expected a single file: ${absolutePath}`);
|
|
6108
|
+
return 1;
|
|
6109
|
+
}
|
|
6110
|
+
if (!/\.md$/i.test(absolutePath)) {
|
|
6111
|
+
error(`[agendex] only Markdown (.md) files are supported: ${absolutePath}`);
|
|
6112
|
+
return 1;
|
|
6113
|
+
}
|
|
6114
|
+
const config = loadConfig();
|
|
6115
|
+
if (!config?.cloudToken || !config?.convexUrl) {
|
|
6116
|
+
error("[agendex] not logged in. Run `agendex login` first.");
|
|
6117
|
+
return 1;
|
|
6118
|
+
}
|
|
6119
|
+
let content;
|
|
6120
|
+
try {
|
|
6121
|
+
content = await readFile8(absolutePath, "utf-8");
|
|
6122
|
+
} catch (err) {
|
|
6123
|
+
error(`[agendex] could not read file: ${err instanceof Error ? err.message : String(err)}`);
|
|
6124
|
+
return 1;
|
|
6125
|
+
}
|
|
6126
|
+
const agentOverride = resolveAgentOverride(args);
|
|
6127
|
+
if (agentOverride === "missing") {
|
|
6128
|
+
error("[agendex] usage: agendex upload <path> [--agent <name>] [--open]");
|
|
6129
|
+
error("[agendex] --agent requires a name");
|
|
6130
|
+
return 1;
|
|
6131
|
+
}
|
|
6132
|
+
const ipAddress = await shouldIncludeLocalIpAddressInSync() ? getLocalIpAddress() : undefined;
|
|
6133
|
+
const payload = fileToSyncPayload(absolutePath, content, {
|
|
6134
|
+
agentOverride,
|
|
6135
|
+
deviceId: config.deviceId ?? loadOrCreateDeviceId(),
|
|
6136
|
+
hostname: osHostname4(),
|
|
6137
|
+
ipAddress,
|
|
6138
|
+
createdAt: stats.birthtime.getTime(),
|
|
6139
|
+
updatedAt: stats.mtime.getTime()
|
|
6140
|
+
});
|
|
6141
|
+
const result = await syncPlanFn(payload);
|
|
6142
|
+
if (!result.ok) {
|
|
6143
|
+
if (result.status === 403) {
|
|
6144
|
+
error(`[agendex] ${result.error ?? "Cloud Pro subscription required"}`);
|
|
6145
|
+
error(`[agendex] View plans and pricing: ${getSiteUrl().replace(/\/$/, "")}/#pricing`);
|
|
6146
|
+
return 1;
|
|
6147
|
+
}
|
|
6148
|
+
error(`[agendex] upload failed: ${result.error ?? "unknown error"}`);
|
|
6149
|
+
return 1;
|
|
6150
|
+
}
|
|
6151
|
+
if (result.skippedLowValue) {
|
|
6152
|
+
log(`[agendex] "${payload.title}" was skipped as a low-value plan and was not stored in the cloud.`);
|
|
6153
|
+
return 0;
|
|
6154
|
+
}
|
|
6155
|
+
const site = getSiteUrl().replace(/\/$/, "");
|
|
6156
|
+
const planUrl = result.planId ? `${site}/dashboard?plan=${encodeURIComponent(result.planId)}` : `${site}/dashboard`;
|
|
6157
|
+
log(`[agendex] uploaded "${payload.title}"`);
|
|
6158
|
+
log(`[agendex] ${planUrl}`);
|
|
6159
|
+
if (args.includes("--open")) {
|
|
6160
|
+
openBrowser2(planUrl, "uploaded plan in your browser");
|
|
6161
|
+
}
|
|
6162
|
+
return 0;
|
|
6163
|
+
}
|
|
6164
|
+
|
|
5768
6165
|
// src/web.ts
|
|
5769
6166
|
async function openAgendexWeb(siteUrlOverride) {
|
|
5770
|
-
const base = siteUrlOverride ??
|
|
6167
|
+
const base = siteUrlOverride ?? getSiteUrl();
|
|
5771
6168
|
const url = base.replace(/\/$/, "");
|
|
5772
6169
|
launchBrowser(url, "Agendex in your browser");
|
|
5773
6170
|
}
|
|
@@ -5802,7 +6199,7 @@ function firstCommandToken(argv) {
|
|
|
5802
6199
|
return;
|
|
5803
6200
|
}
|
|
5804
6201
|
var command = firstCommandToken(args) ?? "start";
|
|
5805
|
-
var cliEntry =
|
|
6202
|
+
var cliEntry = resolve10(process.argv[1] ?? fileURLToPath3(import.meta.url));
|
|
5806
6203
|
async function main() {
|
|
5807
6204
|
const isInternal = args.includes("--daemon") || args.includes("--worker");
|
|
5808
6205
|
if (command === "--version" || command === "-v") {
|
|
@@ -5822,6 +6219,7 @@ async function main() {
|
|
|
5822
6219
|
"add-dir",
|
|
5823
6220
|
"remove-dir",
|
|
5824
6221
|
"list-dirs",
|
|
6222
|
+
"upload",
|
|
5825
6223
|
"upgrade",
|
|
5826
6224
|
"help",
|
|
5827
6225
|
"--help",
|
|
@@ -5922,6 +6320,9 @@ async function main() {
|
|
|
5922
6320
|
await syncAll(force);
|
|
5923
6321
|
return 0;
|
|
5924
6322
|
}
|
|
6323
|
+
case "upload": {
|
|
6324
|
+
return await runUpload(args);
|
|
6325
|
+
}
|
|
5925
6326
|
case "hooks": {
|
|
5926
6327
|
return await runHooksCommand(args, cliEntry);
|
|
5927
6328
|
}
|
|
@@ -6010,7 +6411,7 @@ async function main() {
|
|
|
6010
6411
|
return 1;
|
|
6011
6412
|
}
|
|
6012
6413
|
const resolved = resolveCustomPlanDirPath(dirPath);
|
|
6013
|
-
if (!
|
|
6414
|
+
if (!existsSync13(resolved)) {
|
|
6014
6415
|
writeStderr(`[agendex] path does not exist: ${resolved}`);
|
|
6015
6416
|
return 1;
|
|
6016
6417
|
}
|
|
@@ -6029,7 +6430,7 @@ async function main() {
|
|
|
6029
6430
|
const { request } = await import("node:http");
|
|
6030
6431
|
const body = JSON.stringify({ path: resolved });
|
|
6031
6432
|
try {
|
|
6032
|
-
const res = await new Promise((
|
|
6433
|
+
const res = await new Promise((resolve11, reject) => {
|
|
6033
6434
|
const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
|
|
6034
6435
|
method: "POST",
|
|
6035
6436
|
headers: {
|
|
@@ -6043,7 +6444,7 @@ async function main() {
|
|
|
6043
6444
|
res2.on("data", (chunk) => {
|
|
6044
6445
|
data += chunk;
|
|
6045
6446
|
});
|
|
6046
|
-
res2.on("end", () =>
|
|
6447
|
+
res2.on("end", () => resolve11({ status: res2.statusCode ?? 0, body: data }));
|
|
6047
6448
|
res2.on("error", reject);
|
|
6048
6449
|
});
|
|
6049
6450
|
req.on("error", reject);
|
|
@@ -6203,6 +6604,9 @@ Usage:
|
|
|
6203
6604
|
agendex list-dirs List custom plan directories
|
|
6204
6605
|
agendex sync One-shot scan + sync to cloud (skips unchanged plans)
|
|
6205
6606
|
agendex sync --force Re-sync all plans, ignoring cache
|
|
6607
|
+
agendex upload <path> Upload a single Markdown plan file to the cloud
|
|
6608
|
+
agendex upload <path> --agent <name> Override the uploaded plan's agent label
|
|
6609
|
+
agendex upload <path> --open Open the uploaded plan in the browser after upload
|
|
6206
6610
|
agendex cleanup Interactively remove cloud daemons
|
|
6207
6611
|
agendex cleanup --stale Auto-remove all stale daemons
|
|
6208
6612
|
agendex status Show current config state + daemon status
|
|
@@ -6236,13 +6640,13 @@ function flushStream(stream) {
|
|
|
6236
6640
|
if (stream.destroyed || !stream.writable) {
|
|
6237
6641
|
return Promise.resolve();
|
|
6238
6642
|
}
|
|
6239
|
-
return new Promise((
|
|
6643
|
+
return new Promise((resolve11, reject) => {
|
|
6240
6644
|
stream.write("", (error) => {
|
|
6241
6645
|
if (error) {
|
|
6242
6646
|
reject(error);
|
|
6243
6647
|
return;
|
|
6244
6648
|
}
|
|
6245
|
-
|
|
6649
|
+
resolve11();
|
|
6246
6650
|
});
|
|
6247
6651
|
});
|
|
6248
6652
|
}
|