agendex-cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1194 -182
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1058,14 +1058,14 @@ var init_cleanup = __esm(() => {
1058
1058
  });
1059
1059
 
1060
1060
  // src/cli.ts
1061
- import { spawn as spawn3 } from "node:child_process";
1062
- import { existsSync as existsSync9, statSync as statSync2, writeSync } from "node:fs";
1063
- import { resolve as resolve6 } from "node:path";
1064
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1061
+ import { spawn as spawn4 } from "node:child_process";
1062
+ import { existsSync as existsSync11, statSync as statSync2, writeSync } from "node:fs";
1063
+ import { resolve as resolve8 } from "node:path";
1064
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1065
1065
 
1066
1066
  // ../shared/src/adapters/catalog.ts
1067
- import { homedir as homedir6 } from "node:os";
1068
- import { join as join6 } from "node:path";
1067
+ import { homedir as homedir7 } from "node:os";
1068
+ import { join as join7 } from "node:path";
1069
1069
 
1070
1070
  // ../shared/src/adapters/claude-code.ts
1071
1071
  import { readFile, stat, writeFile } from "node:fs/promises";
@@ -1748,6 +1748,453 @@ var ohMyOpencodeAdapter = {
1748
1748
  }
1749
1749
  };
1750
1750
 
1751
+ // ../shared/src/adapters/plannotator.ts
1752
+ import { existsSync as existsSync3 } from "node:fs";
1753
+ import { readFile as readFile6, stat as stat6 } from "node:fs/promises";
1754
+ import { homedir as homedir6 } from "node:os";
1755
+ import { basename as basename5, dirname, join as join6, resolve as resolve2, sep as sep2 } from "node:path";
1756
+ var REQUEST_TIMEOUT_MS = 5000;
1757
+ var PROJECT_PLANS_DIRNAME = "@plans";
1758
+ function getPlannotatorDir() {
1759
+ return process.env.AGENDEX_PLANNOTATOR_DIR || join6(homedir6(), ".plannotator");
1760
+ }
1761
+ function getPlansDir() {
1762
+ return join6(getPlannotatorDir(), "plans");
1763
+ }
1764
+ function getSessionsDir() {
1765
+ return join6(getPlannotatorDir(), "sessions");
1766
+ }
1767
+ function isRecord3(value) {
1768
+ return typeof value === "object" && value !== null;
1769
+ }
1770
+ function normalizeStatusFromFilename(filePath) {
1771
+ const name = basename5(filePath, ".md");
1772
+ if (name.endsWith("-approved"))
1773
+ return "approved";
1774
+ if (name.endsWith("-denied"))
1775
+ return "denied";
1776
+ return "unknown";
1777
+ }
1778
+ function stripStatusSuffix(title) {
1779
+ return title.replace(/-(approved|denied)$/i, "");
1780
+ }
1781
+ function cleanTitle2(title) {
1782
+ return title.replace(/\d{4}-\d{2}-\d{2}/g, "").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
1783
+ }
1784
+ function extractTitle5(content, filePath) {
1785
+ const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
1786
+ if (heading)
1787
+ return heading.replace(/^Plan:\s*/i, "").trim();
1788
+ const name = stripStatusSuffix(basename5(filePath, ".md"));
1789
+ return cleanTitle2(name) || "Plannotator Plan";
1790
+ }
1791
+ function isAnnotationFile(filePath) {
1792
+ return filePath.endsWith(".annotations.md");
1793
+ }
1794
+ function isSnapshotFile(filePath) {
1795
+ if (!filePath.endsWith(".md"))
1796
+ return false;
1797
+ if (isAnnotationFile(filePath))
1798
+ return false;
1799
+ const status = normalizeStatusFromFilename(filePath);
1800
+ return status === "approved" || status === "denied";
1801
+ }
1802
+ function isProjectPlansPath(filePath) {
1803
+ return resolve2(filePath).split(sep2).includes(PROJECT_PLANS_DIRNAME);
1804
+ }
1805
+ function projectPlansDirForPath(filePath) {
1806
+ const parts = resolve2(filePath).split(sep2);
1807
+ const index = parts.lastIndexOf(PROJECT_PLANS_DIRNAME);
1808
+ if (index === -1)
1809
+ return;
1810
+ return parts.slice(0, index + 1).join(sep2) || sep2;
1811
+ }
1812
+ function isProjectPlanFile(filePath) {
1813
+ return filePath.endsWith(".md") && !isAnnotationFile(filePath) && isProjectPlansPath(filePath);
1814
+ }
1815
+ function isSessionFile2(filePath) {
1816
+ return filePath.endsWith(".json");
1817
+ }
1818
+ function isAlive(pid) {
1819
+ try {
1820
+ process.kill(pid, 0);
1821
+ return true;
1822
+ } catch {
1823
+ return false;
1824
+ }
1825
+ }
1826
+ function isSafePlannotatorUrl(rawUrl) {
1827
+ try {
1828
+ const url = new URL(rawUrl);
1829
+ if (url.protocol !== "http:")
1830
+ return false;
1831
+ if (url.username || url.password)
1832
+ return false;
1833
+ if (url.pathname !== "/" && url.pathname !== "")
1834
+ return false;
1835
+ if (url.search || url.hash)
1836
+ return false;
1837
+ const hostname = url.hostname.toLowerCase();
1838
+ if (hostname === "localhost")
1839
+ return true;
1840
+ if (hostname === "127.0.0.1")
1841
+ return true;
1842
+ if (hostname === "::1" || hostname === "[::1]")
1843
+ return true;
1844
+ return false;
1845
+ } catch {
1846
+ return false;
1847
+ }
1848
+ }
1849
+ function apiUrl(baseUrl, path) {
1850
+ const url = new URL(baseUrl);
1851
+ url.pathname = path;
1852
+ url.search = "";
1853
+ url.hash = "";
1854
+ return url.toString();
1855
+ }
1856
+ async function fetchJson(url, init) {
1857
+ const controller = new AbortController;
1858
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1859
+ try {
1860
+ const res = await fetch(url, {
1861
+ ...init,
1862
+ redirect: "error",
1863
+ signal: controller.signal
1864
+ });
1865
+ if (!res.ok)
1866
+ return null;
1867
+ return await res.json();
1868
+ } catch {
1869
+ return null;
1870
+ } finally {
1871
+ clearTimeout(timer);
1872
+ }
1873
+ }
1874
+ async function postJson(url, body) {
1875
+ const controller = new AbortController;
1876
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1877
+ try {
1878
+ const res = await fetch(url, {
1879
+ method: "POST",
1880
+ headers: { "Content-Type": "application/json" },
1881
+ body: JSON.stringify(body),
1882
+ redirect: "error",
1883
+ signal: controller.signal
1884
+ });
1885
+ return res.ok;
1886
+ } catch {
1887
+ return false;
1888
+ } finally {
1889
+ clearTimeout(timer);
1890
+ }
1891
+ }
1892
+ function normalizeMode(value) {
1893
+ if (value === "plan" || value === "review" || value === "annotate" || value === "archive") {
1894
+ return value;
1895
+ }
1896
+ return;
1897
+ }
1898
+ function parseSessionInfo(raw) {
1899
+ if (!isRecord3(raw))
1900
+ return null;
1901
+ const pid = typeof raw.pid === "number" && Number.isFinite(raw.pid) ? raw.pid : undefined;
1902
+ const port = typeof raw.port === "number" && Number.isFinite(raw.port) ? raw.port : undefined;
1903
+ const url = typeof raw.url === "string" ? raw.url : undefined;
1904
+ if (!pid || !url)
1905
+ return null;
1906
+ return {
1907
+ pid,
1908
+ port,
1909
+ url,
1910
+ mode: normalizeMode(raw.mode),
1911
+ project: typeof raw.project === "string" ? raw.project : undefined,
1912
+ startedAt: typeof raw.startedAt === "string" ? raw.startedAt : undefined,
1913
+ label: typeof raw.label === "string" ? raw.label : undefined,
1914
+ origin: typeof raw.origin === "string" ? raw.origin : undefined,
1915
+ reviewId: typeof raw.reviewId === "string" ? raw.reviewId : undefined,
1916
+ sourcePlanPath: typeof raw.sourcePlanPath === "string" ? raw.sourcePlanPath : undefined
1917
+ };
1918
+ }
1919
+ function parseDate2(value) {
1920
+ if (!value)
1921
+ return;
1922
+ const date = new Date(value);
1923
+ if (Number.isNaN(date.getTime()))
1924
+ return;
1925
+ return date;
1926
+ }
1927
+ function metadataRecord(metadata) {
1928
+ return {
1929
+ source: "plannotator",
1930
+ sourceAdapter: "plannotator",
1931
+ plannotator: metadata
1932
+ };
1933
+ }
1934
+ function annotationsPathForSnapshot(filePath) {
1935
+ const snapshotBase = basename5(filePath, ".md").replace(/-(approved|denied)$/i, "");
1936
+ return join6(dirname(filePath), `${snapshotBase}.annotations.md`);
1937
+ }
1938
+ function snapshotPathsForAnnotation(filePath) {
1939
+ const base = basename5(filePath, ".annotations.md");
1940
+ return [
1941
+ join6(dirname(filePath), `${base}-approved.md`),
1942
+ join6(dirname(filePath), `${base}-denied.md`)
1943
+ ];
1944
+ }
1945
+ function projectPlanPathsForAnnotation(filePath) {
1946
+ const base = basename5(filePath, ".annotations.md");
1947
+ return [join6(dirname(filePath), `${base}.md`), ...snapshotPathsForAnnotation(filePath)];
1948
+ }
1949
+ async function countAnnotationHeadings(filePath) {
1950
+ if (!existsSync3(filePath))
1951
+ return;
1952
+ try {
1953
+ const raw = await readFile6(filePath, "utf-8");
1954
+ const headings = raw.match(/^#{1,6}\s+/gm);
1955
+ return headings?.length ?? (raw.trim() ? 1 : 0);
1956
+ } catch {
1957
+ return;
1958
+ }
1959
+ }
1960
+ async function parseSnapshot(filePath) {
1961
+ try {
1962
+ const content = await readFile6(filePath, "utf-8");
1963
+ const stats = await stat6(filePath);
1964
+ const status = normalizeStatusFromFilename(filePath);
1965
+ const annotationsPath = annotationsPathForSnapshot(filePath);
1966
+ const annotationCount = await countAnnotationHeadings(annotationsPath);
1967
+ const metadata = {
1968
+ kind: "snapshot",
1969
+ mode: "plan",
1970
+ status,
1971
+ annotationsPath: existsSync3(annotationsPath) ? annotationsPath : undefined,
1972
+ annotationCount,
1973
+ writebackCapable: false
1974
+ };
1975
+ return [
1976
+ {
1977
+ id: hashPath(filePath),
1978
+ agent: "plannotator",
1979
+ title: extractTitle5(content, filePath),
1980
+ content,
1981
+ filePath,
1982
+ format: "md",
1983
+ createdAt: stats.birthtime,
1984
+ updatedAt: stats.mtime,
1985
+ metadata: metadataRecord(metadata)
1986
+ }
1987
+ ];
1988
+ } catch {
1989
+ return [];
1990
+ }
1991
+ }
1992
+ async function parseAnnotationCompanion(filePath) {
1993
+ const plans = [];
1994
+ for (const snapshotPath of snapshotPathsForAnnotation(filePath)) {
1995
+ if (!existsSync3(snapshotPath))
1996
+ continue;
1997
+ plans.push(...await parseSnapshot(snapshotPath));
1998
+ }
1999
+ return plans;
2000
+ }
2001
+ async function parseProjectPlan(filePath) {
2002
+ try {
2003
+ const content = await readFile6(filePath, "utf-8");
2004
+ const stats = await stat6(filePath);
2005
+ const projectPlansDir = projectPlansDirForPath(filePath);
2006
+ const projectRoot = projectPlansDir ? dirname(projectPlansDir) : undefined;
2007
+ const status = normalizeStatusFromFilename(filePath);
2008
+ const annotationsPath = annotationsPathForSnapshot(filePath);
2009
+ const annotationCount = await countAnnotationHeadings(annotationsPath);
2010
+ const metadata = {
2011
+ kind: "project-plan",
2012
+ mode: "plan",
2013
+ status,
2014
+ annotationsPath: existsSync3(annotationsPath) ? annotationsPath : undefined,
2015
+ annotationCount,
2016
+ sourcePlanPath: filePath,
2017
+ project: projectRoot ? basename5(projectRoot) : undefined,
2018
+ writebackCapable: false
2019
+ };
2020
+ return [
2021
+ {
2022
+ id: hashPath(filePath),
2023
+ agent: "plannotator",
2024
+ title: extractTitle5(content, filePath),
2025
+ content,
2026
+ filePath,
2027
+ format: "md",
2028
+ createdAt: stats.birthtime,
2029
+ updatedAt: stats.mtime,
2030
+ workspace: projectRoot,
2031
+ metadata: metadataRecord(metadata)
2032
+ }
2033
+ ];
2034
+ } catch {
2035
+ return [];
2036
+ }
2037
+ }
2038
+ async function parseProjectAnnotationCompanion(filePath) {
2039
+ const plans = [];
2040
+ const seen = new Set;
2041
+ for (const planPath of projectPlanPathsForAnnotation(filePath)) {
2042
+ const resolved = resolve2(planPath);
2043
+ if (seen.has(resolved))
2044
+ continue;
2045
+ seen.add(resolved);
2046
+ if (!existsSync3(planPath))
2047
+ continue;
2048
+ plans.push(...await parseProjectPlan(planPath));
2049
+ }
2050
+ return plans;
2051
+ }
2052
+ async function parseLiveSession(filePath) {
2053
+ try {
2054
+ const raw = JSON.parse(await readFile6(filePath, "utf-8"));
2055
+ const session = parseSessionInfo(raw);
2056
+ if (!session?.pid || !session.url)
2057
+ return [];
2058
+ if (!isAlive(session.pid))
2059
+ return [];
2060
+ if (!isSafePlannotatorUrl(session.url))
2061
+ return [];
2062
+ const mode = session.mode ?? "plan";
2063
+ if (mode === "archive")
2064
+ return [];
2065
+ const planResponse = await fetchJson(apiUrl(session.url, "/api/plan"));
2066
+ if (!planResponse?.plan)
2067
+ return [];
2068
+ const stats = await stat6(filePath);
2069
+ const createdAt = parseDate2(session.startedAt) ?? stats.birthtime;
2070
+ const origin = planResponse.origin ?? session.origin;
2071
+ const responseMode = normalizeMode(planResponse.mode) ?? mode;
2072
+ const workspace = planResponse.projectRoot;
2073
+ const sourcePlanPath = session.sourcePlanPath ?? planResponse.filePath;
2074
+ const metadata = {
2075
+ kind: "live-session",
2076
+ mode: responseMode,
2077
+ status: "pending",
2078
+ origin,
2079
+ url: session.url,
2080
+ pid: session.pid,
2081
+ port: session.port,
2082
+ project: session.project ?? planResponse.versionInfo?.project,
2083
+ label: session.label,
2084
+ reviewId: session.reviewId,
2085
+ sessionPath: filePath,
2086
+ sourcePlanPath,
2087
+ startedAt: session.startedAt,
2088
+ writebackCapable: true
2089
+ };
2090
+ return [
2091
+ {
2092
+ id: hashPath(filePath),
2093
+ agent: origin ?? "plannotator",
2094
+ title: extractTitle5(planResponse.plan, session.label ?? filePath),
2095
+ content: planResponse.plan,
2096
+ filePath: sourcePlanPath ?? filePath,
2097
+ format: "md",
2098
+ createdAt,
2099
+ updatedAt: stats.mtime,
2100
+ workspace,
2101
+ metadata: metadataRecord(metadata)
2102
+ }
2103
+ ];
2104
+ } catch {
2105
+ return [];
2106
+ }
2107
+ }
2108
+ function getPlannotatorMetadata(plan) {
2109
+ const metadata = plan.metadata.plannotator;
2110
+ if (!isRecord3(metadata))
2111
+ return;
2112
+ return metadata;
2113
+ }
2114
+ function formatWritebackFeedback(_plan, payload) {
2115
+ const sections = ["# Agendex Plan Feedback", "The user reviewed this plan in Agendex Cloud."];
2116
+ if (payload.feedback.trim()) {
2117
+ sections.push("## Feedback", payload.feedback.trim());
2118
+ }
2119
+ if (payload.revisedContent?.trim()) {
2120
+ sections.push("## Requested revision", "Use this revised plan content as the target shape when you update and resubmit the plan:", payload.revisedContent.trim());
2121
+ }
2122
+ if (payload.annotations?.length) {
2123
+ sections.push("## Typed annotations", JSON.stringify(payload.annotations, null, 2));
2124
+ }
2125
+ sections.push("Please revise the plan and resubmit it for Plannotator review.");
2126
+ return sections.join(`
2127
+
2128
+ `);
2129
+ }
2130
+ function annotationsForEndpoint(annotations) {
2131
+ return annotations ?? [];
2132
+ }
2133
+ var plannotatorAdapter = {
2134
+ agent: "plannotator",
2135
+ writable: false,
2136
+ getSearchPaths() {
2137
+ return [getPlansDir(), getSessionsDir()];
2138
+ },
2139
+ getWatchPaths() {
2140
+ return [getPlansDir(), getSessionsDir()];
2141
+ },
2142
+ matches(filePath) {
2143
+ const normalized = resolve2(filePath);
2144
+ const plansDir2 = resolve2(getPlansDir());
2145
+ const sessionsDir2 = resolve2(getSessionsDir());
2146
+ if (normalized.startsWith(plansDir2 + sep2))
2147
+ return isSnapshotFile(filePath) || isAnnotationFile(filePath);
2148
+ if (normalized.startsWith(sessionsDir2 + sep2))
2149
+ return isSessionFile2(filePath);
2150
+ if (isProjectPlansPath(filePath))
2151
+ return isProjectPlanFile(filePath) || isAnnotationFile(filePath);
2152
+ return false;
2153
+ },
2154
+ async parse(filePath) {
2155
+ const normalized = resolve2(filePath);
2156
+ const plansDir2 = resolve2(getPlansDir());
2157
+ const sessionsDir2 = resolve2(getSessionsDir());
2158
+ if (normalized.startsWith(plansDir2 + sep2) && isSnapshotFile(filePath)) {
2159
+ return await parseSnapshot(filePath);
2160
+ }
2161
+ if (normalized.startsWith(plansDir2 + sep2) && isAnnotationFile(filePath)) {
2162
+ return await parseAnnotationCompanion(filePath);
2163
+ }
2164
+ if (normalized.startsWith(sessionsDir2 + sep2) && isSessionFile2(filePath)) {
2165
+ return await parseLiveSession(filePath);
2166
+ }
2167
+ if (isProjectPlanFile(filePath)) {
2168
+ return await parseProjectPlan(filePath);
2169
+ }
2170
+ if (isProjectPlansPath(filePath) && isAnnotationFile(filePath)) {
2171
+ return await parseProjectAnnotationCompanion(filePath);
2172
+ }
2173
+ return [];
2174
+ },
2175
+ async write() {
2176
+ return false;
2177
+ },
2178
+ async requestChanges(plan, payload) {
2179
+ const metadata = getPlannotatorMetadata(plan);
2180
+ if (!metadata?.url || !metadata.writebackCapable)
2181
+ return false;
2182
+ if (!isSafePlannotatorUrl(metadata.url))
2183
+ return false;
2184
+ const feedback = formatWritebackFeedback(plan, payload);
2185
+ if (metadata.mode === "review" || metadata.mode === "annotate") {
2186
+ return await postJson(apiUrl(metadata.url, "/api/feedback"), {
2187
+ feedback,
2188
+ annotations: annotationsForEndpoint(payload.annotations)
2189
+ });
2190
+ }
2191
+ return await postJson(apiUrl(metadata.url, "/api/deny"), {
2192
+ feedback,
2193
+ planSave: { enabled: true }
2194
+ });
2195
+ }
2196
+ };
2197
+
1751
2198
  // ../shared/src/adapters/stub.ts
1752
2199
  function createStubAdapter(agent, searchPaths, matchExt) {
1753
2200
  return {
@@ -1762,7 +2209,7 @@ function createStubAdapter(agent, searchPaths, matchExt) {
1762
2209
  }
1763
2210
 
1764
2211
  // ../shared/src/adapters/catalog.ts
1765
- var home = homedir6();
2212
+ var home = homedir7();
1766
2213
  var ADAPTER_AGENT_ALIASES = {
1767
2214
  amp: "amp",
1768
2215
  antigravity: "antigravity",
@@ -1792,6 +2239,7 @@ var ADAPTER_AGENT_ALIASES = {
1792
2239
  opencode: "oh-my-opencode",
1793
2240
  openhands: "openhands",
1794
2241
  pi: "pi",
2242
+ plannotator: "plannotator",
1795
2243
  qoder: "qoder",
1796
2244
  "qwen-code": "qwen-code",
1797
2245
  replit: "replit",
@@ -1812,7 +2260,7 @@ var CATALOG = [
1812
2260
  group: "universal",
1813
2261
  implemented: false,
1814
2262
  defaultEnabled: true,
1815
- createAdapter: () => createStubAdapter("amp", [join6(home, ".amp")], ".json")
2263
+ createAdapter: () => createStubAdapter("amp", [join7(home, ".amp")], ".json")
1816
2264
  },
1817
2265
  {
1818
2266
  id: "antigravity",
@@ -1820,7 +2268,7 @@ var CATALOG = [
1820
2268
  group: "other",
1821
2269
  implemented: false,
1822
2270
  defaultEnabled: false,
1823
- createAdapter: () => createStubAdapter("antigravity", [join6(home, ".gemini", "antigravity")], ".json")
2271
+ createAdapter: () => createStubAdapter("antigravity", [join7(home, ".gemini", "antigravity")], ".json")
1824
2272
  },
1825
2273
  {
1826
2274
  id: "augment",
@@ -1828,7 +2276,7 @@ var CATALOG = [
1828
2276
  group: "other",
1829
2277
  implemented: false,
1830
2278
  defaultEnabled: false,
1831
- createAdapter: () => createStubAdapter("augment", [join6(home, ".augment")], ".json")
2279
+ createAdapter: () => createStubAdapter("augment", [join7(home, ".augment")], ".json")
1832
2280
  },
1833
2281
  {
1834
2282
  id: "claude-code",
@@ -1844,7 +2292,7 @@ var CATALOG = [
1844
2292
  group: "other",
1845
2293
  implemented: false,
1846
2294
  defaultEnabled: false,
1847
- createAdapter: () => createStubAdapter("openclaw", [join6(home, ".openclaw")], ".md")
2295
+ createAdapter: () => createStubAdapter("openclaw", [join7(home, ".openclaw")], ".md")
1848
2296
  },
1849
2297
  {
1850
2298
  id: "cline",
@@ -1853,8 +2301,8 @@ var CATALOG = [
1853
2301
  implemented: false,
1854
2302
  defaultEnabled: true,
1855
2303
  createAdapter: () => createStubAdapter("cline", [
1856
- join6(home, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev"),
1857
- join6(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
2304
+ join7(home, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev"),
2305
+ join7(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
1858
2306
  ], ".json")
1859
2307
  },
1860
2308
  {
@@ -1863,7 +2311,7 @@ var CATALOG = [
1863
2311
  group: "other",
1864
2312
  implemented: false,
1865
2313
  defaultEnabled: false,
1866
- createAdapter: () => createStubAdapter("codebuddy", [join6(home, ".codebuddy")], ".md")
2314
+ createAdapter: () => createStubAdapter("codebuddy", [join7(home, ".codebuddy")], ".md")
1867
2315
  },
1868
2316
  {
1869
2317
  id: "codex",
@@ -1879,7 +2327,7 @@ var CATALOG = [
1879
2327
  group: "other",
1880
2328
  implemented: false,
1881
2329
  defaultEnabled: false,
1882
- createAdapter: () => createStubAdapter("command-code", [join6(home, ".commandcode")], ".md")
2330
+ createAdapter: () => createStubAdapter("command-code", [join7(home, ".commandcode")], ".md")
1883
2331
  },
1884
2332
  {
1885
2333
  id: "continue",
@@ -1895,7 +2343,7 @@ var CATALOG = [
1895
2343
  group: "other",
1896
2344
  implemented: false,
1897
2345
  defaultEnabled: false,
1898
- createAdapter: () => createStubAdapter("crush", [join6(home, ".config", "crush")], ".json")
2346
+ createAdapter: () => createStubAdapter("crush", [join7(home, ".config", "crush")], ".json")
1899
2347
  },
1900
2348
  {
1901
2349
  id: "cursor",
@@ -1911,7 +2359,7 @@ var CATALOG = [
1911
2359
  group: "other",
1912
2360
  implemented: false,
1913
2361
  defaultEnabled: true,
1914
- createAdapter: () => createStubAdapter("droid", [join6(home, ".factory", "droids")], ".md")
2362
+ createAdapter: () => createStubAdapter("droid", [join7(home, ".factory", "droids")], ".md")
1915
2363
  },
1916
2364
  {
1917
2365
  id: "gemini-cli",
@@ -1919,7 +2367,7 @@ var CATALOG = [
1919
2367
  group: "universal",
1920
2368
  implemented: false,
1921
2369
  defaultEnabled: false,
1922
- createAdapter: () => createStubAdapter("gemini-cli", [join6(home, ".gemini")], ".md")
2370
+ createAdapter: () => createStubAdapter("gemini-cli", [join7(home, ".gemini")], ".md")
1923
2371
  },
1924
2372
  {
1925
2373
  id: "github-copilot",
@@ -1927,7 +2375,7 @@ var CATALOG = [
1927
2375
  group: "universal",
1928
2376
  implemented: false,
1929
2377
  defaultEnabled: true,
1930
- createAdapter: () => createStubAdapter("copilot-chat", [join6(home, ".vscode", "User", "workspaceStorage")], ".json")
2378
+ createAdapter: () => createStubAdapter("copilot-chat", [join7(home, ".vscode", "User", "workspaceStorage")], ".json")
1931
2379
  },
1932
2380
  {
1933
2381
  id: "goose",
@@ -1935,7 +2383,7 @@ var CATALOG = [
1935
2383
  group: "other",
1936
2384
  implemented: false,
1937
2385
  defaultEnabled: false,
1938
- createAdapter: () => createStubAdapter("goose", [join6(home, ".config", "goose")], ".json")
2386
+ createAdapter: () => createStubAdapter("goose", [join7(home, ".config", "goose")], ".json")
1939
2387
  },
1940
2388
  {
1941
2389
  id: "junie",
@@ -1943,7 +2391,7 @@ var CATALOG = [
1943
2391
  group: "other",
1944
2392
  implemented: false,
1945
2393
  defaultEnabled: false,
1946
- createAdapter: () => createStubAdapter("junie", [join6(home, ".junie")], ".json")
2394
+ createAdapter: () => createStubAdapter("junie", [join7(home, ".junie")], ".json")
1947
2395
  },
1948
2396
  {
1949
2397
  id: "iflow-cli",
@@ -1951,7 +2399,7 @@ var CATALOG = [
1951
2399
  group: "other",
1952
2400
  implemented: false,
1953
2401
  defaultEnabled: false,
1954
- createAdapter: () => createStubAdapter("iflow-cli", [join6(home, ".iflow")], ".json")
2402
+ createAdapter: () => createStubAdapter("iflow-cli", [join7(home, ".iflow")], ".json")
1955
2403
  },
1956
2404
  {
1957
2405
  id: "kilo",
@@ -1959,7 +2407,7 @@ var CATALOG = [
1959
2407
  group: "other",
1960
2408
  implemented: false,
1961
2409
  defaultEnabled: true,
1962
- createAdapter: () => createStubAdapter("kilo-cli", [join6(home, ".kilo")], ".md")
2410
+ createAdapter: () => createStubAdapter("kilo-cli", [join7(home, ".kilo")], ".md")
1963
2411
  },
1964
2412
  {
1965
2413
  id: "kimi-cli",
@@ -1967,7 +2415,7 @@ var CATALOG = [
1967
2415
  group: "universal",
1968
2416
  implemented: false,
1969
2417
  defaultEnabled: false,
1970
- createAdapter: () => createStubAdapter("kimi-cli", [join6(home, ".kimi")], ".md")
2418
+ createAdapter: () => createStubAdapter("kimi-cli", [join7(home, ".kimi")], ".md")
1971
2419
  },
1972
2420
  {
1973
2421
  id: "kiro-cli",
@@ -1975,7 +2423,7 @@ var CATALOG = [
1975
2423
  group: "other",
1976
2424
  implemented: false,
1977
2425
  defaultEnabled: false,
1978
- createAdapter: () => createStubAdapter("kiro-cli", [join6(home, ".kiro")], ".json")
2426
+ createAdapter: () => createStubAdapter("kiro-cli", [join7(home, ".kiro")], ".json")
1979
2427
  },
1980
2428
  {
1981
2429
  id: "kode",
@@ -1983,7 +2431,7 @@ var CATALOG = [
1983
2431
  group: "other",
1984
2432
  implemented: false,
1985
2433
  defaultEnabled: false,
1986
- createAdapter: () => createStubAdapter("kode", [join6(home, ".kode")], ".json")
2434
+ createAdapter: () => createStubAdapter("kode", [join7(home, ".kode")], ".json")
1987
2435
  },
1988
2436
  {
1989
2437
  id: "mcpjam",
@@ -1991,7 +2439,7 @@ var CATALOG = [
1991
2439
  group: "other",
1992
2440
  implemented: false,
1993
2441
  defaultEnabled: false,
1994
- createAdapter: () => createStubAdapter("mcpjam", [join6(home, ".mcpjam")], ".json")
2442
+ createAdapter: () => createStubAdapter("mcpjam", [join7(home, ".mcpjam")], ".json")
1995
2443
  },
1996
2444
  {
1997
2445
  id: "mistral-vibe",
@@ -1999,7 +2447,7 @@ var CATALOG = [
1999
2447
  group: "other",
2000
2448
  implemented: false,
2001
2449
  defaultEnabled: false,
2002
- createAdapter: () => createStubAdapter("mistral-vibe", [join6(home, ".vibe")], ".json")
2450
+ createAdapter: () => createStubAdapter("mistral-vibe", [join7(home, ".vibe")], ".json")
2003
2451
  },
2004
2452
  {
2005
2453
  id: "mux",
@@ -2007,7 +2455,7 @@ var CATALOG = [
2007
2455
  group: "other",
2008
2456
  implemented: false,
2009
2457
  defaultEnabled: false,
2010
- createAdapter: () => createStubAdapter("mux", [join6(home, ".mux")], ".json")
2458
+ createAdapter: () => createStubAdapter("mux", [join7(home, ".mux")], ".json")
2011
2459
  },
2012
2460
  {
2013
2461
  id: "opencode",
@@ -2023,7 +2471,7 @@ var CATALOG = [
2023
2471
  group: "other",
2024
2472
  implemented: false,
2025
2473
  defaultEnabled: false,
2026
- createAdapter: () => createStubAdapter("openhands", [join6(home, ".openhands")], ".json")
2474
+ createAdapter: () => createStubAdapter("openhands", [join7(home, ".openhands")], ".json")
2027
2475
  },
2028
2476
  {
2029
2477
  id: "pi",
@@ -2031,7 +2479,15 @@ var CATALOG = [
2031
2479
  group: "other",
2032
2480
  implemented: false,
2033
2481
  defaultEnabled: false,
2034
- createAdapter: () => createStubAdapter("pi", [join6(home, ".pi", "agent")], ".md")
2482
+ createAdapter: () => createStubAdapter("pi", [join7(home, ".pi", "agent")], ".md")
2483
+ },
2484
+ {
2485
+ id: "plannotator",
2486
+ displayName: "Plannotator",
2487
+ group: "universal",
2488
+ implemented: true,
2489
+ defaultEnabled: false,
2490
+ createAdapter: () => plannotatorAdapter
2035
2491
  },
2036
2492
  {
2037
2493
  id: "qoder",
@@ -2039,7 +2495,7 @@ var CATALOG = [
2039
2495
  group: "other",
2040
2496
  implemented: false,
2041
2497
  defaultEnabled: false,
2042
- createAdapter: () => createStubAdapter("qoder", [join6(home, ".qoder")], ".json")
2498
+ createAdapter: () => createStubAdapter("qoder", [join7(home, ".qoder")], ".json")
2043
2499
  },
2044
2500
  {
2045
2501
  id: "qwen-code",
@@ -2047,7 +2503,7 @@ var CATALOG = [
2047
2503
  group: "other",
2048
2504
  implemented: false,
2049
2505
  defaultEnabled: false,
2050
- createAdapter: () => createStubAdapter("qwen-code", [join6(home, ".qwen")], ".json")
2506
+ createAdapter: () => createStubAdapter("qwen-code", [join7(home, ".qwen")], ".json")
2051
2507
  },
2052
2508
  {
2053
2509
  id: "replit",
@@ -2055,7 +2511,7 @@ var CATALOG = [
2055
2511
  group: "other",
2056
2512
  implemented: false,
2057
2513
  defaultEnabled: false,
2058
- createAdapter: () => createStubAdapter("replit", [join6(home, ".replit")], ".md")
2514
+ createAdapter: () => createStubAdapter("replit", [join7(home, ".replit")], ".md")
2059
2515
  },
2060
2516
  {
2061
2517
  id: "roo",
@@ -2063,7 +2519,7 @@ var CATALOG = [
2063
2519
  group: "other",
2064
2520
  implemented: false,
2065
2521
  defaultEnabled: false,
2066
- createAdapter: () => createStubAdapter("roo", [join6(home, ".roo")], ".md")
2522
+ createAdapter: () => createStubAdapter("roo", [join7(home, ".roo")], ".md")
2067
2523
  },
2068
2524
  {
2069
2525
  id: "trae",
@@ -2071,7 +2527,7 @@ var CATALOG = [
2071
2527
  group: "other",
2072
2528
  implemented: false,
2073
2529
  defaultEnabled: false,
2074
- createAdapter: () => createStubAdapter("trae", [join6(home, ".trae")], ".md")
2530
+ createAdapter: () => createStubAdapter("trae", [join7(home, ".trae")], ".md")
2075
2531
  },
2076
2532
  {
2077
2533
  id: "trae-cn",
@@ -2079,7 +2535,7 @@ var CATALOG = [
2079
2535
  group: "other",
2080
2536
  implemented: false,
2081
2537
  defaultEnabled: false,
2082
- createAdapter: () => createStubAdapter("trae-cn", [join6(home, ".trae-cn")], ".md")
2538
+ createAdapter: () => createStubAdapter("trae-cn", [join7(home, ".trae-cn")], ".md")
2083
2539
  },
2084
2540
  {
2085
2541
  id: "windsurf",
@@ -2087,7 +2543,7 @@ var CATALOG = [
2087
2543
  group: "other",
2088
2544
  implemented: false,
2089
2545
  defaultEnabled: true,
2090
- createAdapter: () => createStubAdapter("windsurf", [join6(home, ".cascade_backups")], ".md")
2546
+ createAdapter: () => createStubAdapter("windsurf", [join7(home, ".cascade_backups")], ".md")
2091
2547
  },
2092
2548
  {
2093
2549
  id: "zencoder",
@@ -2095,7 +2551,7 @@ var CATALOG = [
2095
2551
  group: "other",
2096
2552
  implemented: false,
2097
2553
  defaultEnabled: false,
2098
- createAdapter: () => createStubAdapter("zencoder", [join6(home, ".zencoder")], ".json")
2554
+ createAdapter: () => createStubAdapter("zencoder", [join7(home, ".zencoder")], ".json")
2099
2555
  },
2100
2556
  {
2101
2557
  id: "neovate",
@@ -2103,7 +2559,7 @@ var CATALOG = [
2103
2559
  group: "other",
2104
2560
  implemented: false,
2105
2561
  defaultEnabled: false,
2106
- createAdapter: () => createStubAdapter("neovate", [join6(home, ".neovate")], ".json")
2562
+ createAdapter: () => createStubAdapter("neovate", [join7(home, ".neovate")], ".json")
2107
2563
  },
2108
2564
  {
2109
2565
  id: "pochi",
@@ -2111,7 +2567,7 @@ var CATALOG = [
2111
2567
  group: "other",
2112
2568
  implemented: false,
2113
2569
  defaultEnabled: false,
2114
- createAdapter: () => createStubAdapter("pochi", [join6(home, ".pochi")], ".json")
2570
+ createAdapter: () => createStubAdapter("pochi", [join7(home, ".pochi")], ".json")
2115
2571
  },
2116
2572
  {
2117
2573
  id: "adal",
@@ -2119,7 +2575,7 @@ var CATALOG = [
2119
2575
  group: "other",
2120
2576
  implemented: false,
2121
2577
  defaultEnabled: false,
2122
- createAdapter: () => createStubAdapter("adal", [join6(home, ".adal")], ".json")
2578
+ createAdapter: () => createStubAdapter("adal", [join7(home, ".adal")], ".json")
2123
2579
  },
2124
2580
  {
2125
2581
  id: "aider",
@@ -2127,7 +2583,7 @@ var CATALOG = [
2127
2583
  group: "other",
2128
2584
  implemented: false,
2129
2585
  defaultEnabled: true,
2130
- createAdapter: () => createStubAdapter("aider", [join6(home, ".aider")], ".aider.chat.history.md")
2586
+ createAdapter: () => createStubAdapter("aider", [join7(home, ".aider")], ".aider.chat.history.md")
2131
2587
  }
2132
2588
  ];
2133
2589
  function getAdapterCatalog() {
@@ -2198,9 +2654,9 @@ function getActiveAdapters() {
2198
2654
  var activeAdapters = resolveAdapters(getDefaultAdapterIds());
2199
2655
  // ../shared/src/config.ts
2200
2656
  import { randomBytes } from "node:crypto";
2201
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2202
- import { homedir as homedir7 } from "node:os";
2203
- import { join as join7, resolve as resolve2 } from "node:path";
2657
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2658
+ import { homedir as homedir8 } from "node:os";
2659
+ import { join as join8, resolve as resolve3 } from "node:path";
2204
2660
 
2205
2661
  // ../shared/src/setup/adapter-selection.ts
2206
2662
  init_dist2();
@@ -2264,7 +2720,7 @@ function getHomeDir() {
2264
2720
  if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
2265
2721
  return `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`;
2266
2722
  }
2267
- return homedir7();
2723
+ return homedir8();
2268
2724
  }
2269
2725
  function setDevMode(dev) {
2270
2726
  devModeOverride = dev;
@@ -2275,16 +2731,19 @@ function isDevMode() {
2275
2731
  return process.env.AGENDEX_DEV === "1";
2276
2732
  }
2277
2733
  function getConfigDir() {
2278
- return join7(getHomeDir(), isDevMode() ? ".agendex-dev" : ".agendex");
2734
+ const override = process.env.AGENDEX_CONFIG_DIR?.trim();
2735
+ if (override)
2736
+ return resolve3(expandHomePath(override));
2737
+ return join8(getHomeDir(), isDevMode() ? ".agendex-dev" : ".agendex");
2279
2738
  }
2280
2739
  function ensureConfigDir() {
2281
2740
  const dir = getConfigDir();
2282
- if (!existsSync3(dir))
2741
+ if (!existsSync4(dir))
2283
2742
  mkdirSync(dir, { recursive: true });
2284
2743
  }
2285
2744
  function readStoredConfig() {
2286
2745
  const cfgPath = getConfigPath();
2287
- if (!existsSync3(cfgPath))
2746
+ if (!existsSync4(cfgPath))
2288
2747
  return null;
2289
2748
  try {
2290
2749
  const raw = JSON.parse(readFileSync3(cfgPath, "utf-8"));
@@ -2302,7 +2761,7 @@ function normalizeAdapterIds(input) {
2302
2761
  }
2303
2762
  function expandHomePath(p) {
2304
2763
  if (p.startsWith("~/") || p === "~")
2305
- return join7(getHomeDir(), p.slice(1));
2764
+ return join8(getHomeDir(), p.slice(1));
2306
2765
  return p;
2307
2766
  }
2308
2767
  function resolveCustomPlanDirPath(userPath) {
@@ -2310,7 +2769,7 @@ function resolveCustomPlanDirPath(userPath) {
2310
2769
  if (!trimmed) {
2311
2770
  throw new Error("Custom plan directory path must not be empty");
2312
2771
  }
2313
- return resolve2(expandHomePath(trimmed));
2772
+ return resolve3(expandHomePath(trimmed));
2314
2773
  }
2315
2774
  function normalizeCustomPlanDirs(input) {
2316
2775
  if (!Array.isArray(input))
@@ -2375,6 +2834,7 @@ function loadOrCreateToken() {
2375
2834
  return existing.token;
2376
2835
  const token = generateToken();
2377
2836
  saveConfig({
2837
+ ...existing ?? {},
2378
2838
  configVersion: 3,
2379
2839
  token,
2380
2840
  enabledAdapters: existing?.enabledAdapters ?? [],
@@ -2401,7 +2861,7 @@ function loadOrCreateDeviceId() {
2401
2861
  return deviceId;
2402
2862
  }
2403
2863
  function getConfigPath() {
2404
- return join7(getConfigDir(), "config.json");
2864
+ return join8(getConfigDir(), "config.json");
2405
2865
  }
2406
2866
  async function loadOrInitConfig(options = {}) {
2407
2867
  const configureAdapters = Boolean(options.configureAdapters);
@@ -2448,9 +2908,10 @@ async function loadOrInitConfig(options = {}) {
2448
2908
  var CLI_DAEMON_HEARTBEAT_INTERVAL_MS = 30000;
2449
2909
  var CLI_DAEMON_STALE_AFTER_MS = CLI_DAEMON_HEARTBEAT_INTERVAL_MS * 5;
2450
2910
  // ../shared/src/services/plan-service.ts
2451
- import { existsSync as existsSync4, readdirSync as readdirSync3, statSync } from "node:fs";
2452
- import { lstat, mkdir, readdir as readdir2, readFile as readFile6, stat as stat6, writeFile as writeFile3 } from "node:fs/promises";
2453
- import { join as join8, resolve as resolve3, sep as sep2 } from "node:path";
2911
+ import { existsSync as existsSync5, readdirSync as readdirSync3, realpathSync, statSync } from "node:fs";
2912
+ import { lstat, mkdir, readdir as readdir2, readFile as readFile7, stat as stat7, writeFile as writeFile3 } from "node:fs/promises";
2913
+ import { homedir as homedir9 } from "node:os";
2914
+ import { dirname as dirname2, join as join9, resolve as resolve4, sep as sep3 } from "node:path";
2454
2915
 
2455
2916
  // ../shared/src/services/plan-value.ts
2456
2917
  function isLowValuePlan(plan) {
@@ -2780,12 +3241,15 @@ function annotatePlanValueMetadata(plan) {
2780
3241
 
2781
3242
  // ../shared/src/services/plan-service.ts
2782
3243
  function getUserPlansDir() {
2783
- return join8(getConfigDir(), "plans");
3244
+ return join9(getConfigDir(), "plans");
2784
3245
  }
2785
3246
  var store = new Map;
2786
3247
  var MAX_DEPTH = 6;
2787
3248
  var DISCOVERY_MAX_DEPTH = 4;
2788
- var PROJECT_PLAN_MARKERS = [{ marker: ".sisyphus/plans", agent: "oh-my-opencode" }];
3249
+ var PROJECT_PLAN_MARKERS = [
3250
+ { marker: ".sisyphus/plans", agent: "oh-my-opencode" },
3251
+ { marker: "@plans", agent: "plannotator" }
3252
+ ];
2789
3253
  var SKIP_DIRS = new Set([
2790
3254
  "node_modules",
2791
3255
  ".git",
@@ -2818,19 +3282,58 @@ var SKIP_DIRS = new Set([
2818
3282
  "Google Drive",
2819
3283
  "iCloud Drive"
2820
3284
  ]);
3285
+ function getRuntimeHomeDir() {
3286
+ const homeFromEnv = process.env.HOME || process.env.USERPROFILE || (process.env.HOMEDRIVE && process.env.HOMEPATH ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` : undefined);
3287
+ return resolve4(homeFromEnv || homedir9());
3288
+ }
2821
3289
  function discoverProjectPlanDirs() {
2822
- const home2 = getHomeDir();
3290
+ const home2 = canonicalPath(getRuntimeHomeDir());
2823
3291
  const results = [];
2824
- function walk(dir, depth) {
2825
- if (depth > DISCOVERY_MAX_DEPTH)
3292
+ const seen = new Set;
3293
+ let nearestAncestorMarkerRoot;
3294
+ function addResult(dir, agent) {
3295
+ const resolved = canonicalPath(dir);
3296
+ const key = `${agent}:${resolved}`;
3297
+ if (seen.has(key))
2826
3298
  return;
3299
+ seen.add(key);
3300
+ results.push({ dir: resolved, agent });
3301
+ }
3302
+ function inspectMarkers(dir) {
3303
+ let found = false;
2827
3304
  for (const { marker, agent } of PROJECT_PLAN_MARKERS) {
2828
- const candidate = join8(dir, marker);
2829
- if (existsSync4(candidate)) {
2830
- results.push({ dir: candidate, agent });
3305
+ const candidate = join9(dir, marker);
3306
+ if (existsSync5(candidate)) {
3307
+ addResult(candidate, agent);
3308
+ found = true;
3309
+ }
3310
+ }
3311
+ return found;
3312
+ }
3313
+ function walkAncestorsForCurrentProject() {
3314
+ let dir = canonicalPath(process.cwd());
3315
+ while (true) {
3316
+ if (inspectMarkers(dir)) {
3317
+ nearestAncestorMarkerRoot = dir;
2831
3318
  return;
2832
3319
  }
3320
+ if (dir === home2)
3321
+ return;
3322
+ const parent = dirname2(dir);
3323
+ if (parent === dir)
3324
+ return;
3325
+ dir = parent;
2833
3326
  }
3327
+ }
3328
+ function isAncestorOfNearestMarker(dir) {
3329
+ return Boolean(nearestAncestorMarkerRoot?.startsWith(dir + sep3));
3330
+ }
3331
+ function walk(dir, depth) {
3332
+ if (depth > DISCOVERY_MAX_DEPTH)
3333
+ return;
3334
+ const resolved = canonicalPath(dir);
3335
+ if (!isAncestorOfNearestMarker(resolved) && inspectMarkers(dir))
3336
+ return;
2834
3337
  let names;
2835
3338
  try {
2836
3339
  names = readdirSync3(dir);
@@ -2842,13 +3345,14 @@ function discoverProjectPlanDirs() {
2842
3345
  continue;
2843
3346
  if (SKIP_DIRS.has(name))
2844
3347
  continue;
2845
- const full = join8(dir, name);
3348
+ const full = join9(dir, name);
2846
3349
  try {
2847
3350
  if (statSync(full).isDirectory())
2848
3351
  walk(full, depth + 1);
2849
3352
  } catch {}
2850
3353
  }
2851
3354
  }
3355
+ walkAncestorsForCurrentProject();
2852
3356
  walk(home2, 0);
2853
3357
  return results;
2854
3358
  }
@@ -2862,9 +3366,9 @@ function notifyPlansChanged() {
2862
3366
  async function walkDir(dir, depth = 0, seen = new Set) {
2863
3367
  if (depth > MAX_DEPTH)
2864
3368
  return [];
2865
- if (!existsSync4(dir))
3369
+ if (!existsSync5(dir))
2866
3370
  return [];
2867
- const real = resolve3(dir);
3371
+ const real = resolve4(dir);
2868
3372
  if (seen.has(real))
2869
3373
  return [];
2870
3374
  seen.add(real);
@@ -2872,7 +3376,7 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2872
3376
  try {
2873
3377
  const entries = await readdir2(dir, { withFileTypes: true });
2874
3378
  for (const entry of entries) {
2875
- const full = join8(dir, entry.name);
3379
+ const full = join9(dir, entry.name);
2876
3380
  try {
2877
3381
  const stats = await lstat(full);
2878
3382
  if (stats.isSymbolicLink())
@@ -2889,8 +3393,8 @@ async function walkDir(dir, depth = 0, seen = new Set) {
2889
3393
  }
2890
3394
  async function parseGenericMarkdownPlan(filePath, extraMetadata) {
2891
3395
  try {
2892
- const content = await readFile6(filePath, "utf-8");
2893
- const stats = await stat6(filePath);
3396
+ const content = await readFile7(filePath, "utf-8");
3397
+ const stats = await stat7(filePath);
2894
3398
  let agent = (typeof extraMetadata.agentHint === "string" ? extraMetadata.agentHint : "") || "unknown";
2895
3399
  const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
2896
3400
  if (fmMatch) {
@@ -2918,7 +3422,7 @@ async function parseGenericMarkdownPlan(filePath, extraMetadata) {
2918
3422
  }
2919
3423
  async function scanUserPlans(into) {
2920
3424
  const userPlansDir = getUserPlansDir();
2921
- if (!existsSync4(userPlansDir))
3425
+ if (!existsSync5(userPlansDir))
2922
3426
  return;
2923
3427
  const files = await walkDir(userPlansDir);
2924
3428
  for (const file of files) {
@@ -2932,19 +3436,27 @@ async function scanUserPlans(into) {
2932
3436
  function getCustomPlanDirs() {
2933
3437
  return loadConfig()?.customPlanDirs ?? [];
2934
3438
  }
3439
+ function canonicalPath(path) {
3440
+ const resolved = resolve4(path);
3441
+ try {
3442
+ return realpathSync(resolved);
3443
+ } catch {
3444
+ return resolved;
3445
+ }
3446
+ }
2935
3447
  function pathsOverlapFilesystemTree(a, b) {
2936
- const ra = resolve3(a);
2937
- const rb = resolve3(b);
3448
+ const ra = canonicalPath(a);
3449
+ const rb = canonicalPath(b);
2938
3450
  if (ra === rb)
2939
3451
  return true;
2940
- if (ra.startsWith(rb + sep2))
3452
+ if (ra.startsWith(rb + sep3))
2941
3453
  return true;
2942
- if (rb.startsWith(ra + sep2))
3454
+ if (rb.startsWith(ra + sep3))
2943
3455
  return true;
2944
3456
  return false;
2945
3457
  }
2946
3458
  function overlapsAnyRoot(candidate, roots) {
2947
- const resolvedCandidate = resolve3(candidate);
3459
+ const resolvedCandidate = canonicalPath(candidate);
2948
3460
  for (const root of roots) {
2949
3461
  if (pathsOverlapFilesystemTree(resolvedCandidate, root))
2950
3462
  return true;
@@ -2953,9 +3465,9 @@ function overlapsAnyRoot(candidate, roots) {
2953
3465
  }
2954
3466
  async function scanCustomPlanDirs(coveredPaths, into) {
2955
3467
  const dirs = getCustomPlanDirs();
2956
- const userPlansDir = resolve3(getUserPlansDir());
3468
+ const userPlansDir = canonicalPath(getUserPlansDir());
2957
3469
  for (const dir of dirs) {
2958
- const resolved = resolve3(dir);
3470
+ const resolved = canonicalPath(dir);
2959
3471
  if (overlapsAnyRoot(resolved, coveredPaths)) {
2960
3472
  console.log(`[agendex] skipping custom dir (overlaps adapter / discovered coverage): ${dir}`);
2961
3473
  continue;
@@ -2964,7 +3476,7 @@ async function scanCustomPlanDirs(coveredPaths, into) {
2964
3476
  console.log(`[agendex] skipping custom dir (overlaps user plans): ${dir}`);
2965
3477
  continue;
2966
3478
  }
2967
- if (!existsSync4(dir)) {
3479
+ if (!existsSync5(dir)) {
2968
3480
  console.log(`[agendex] skipping custom dir (not found): ${dir}`);
2969
3481
  continue;
2970
3482
  }
@@ -2993,7 +3505,7 @@ async function scan() {
2993
3505
  const coveredPaths = new Set;
2994
3506
  for (const adapter of adapters) {
2995
3507
  for (const searchPath of adapter.getSearchPaths()) {
2996
- coveredPaths.add(resolve3(searchPath));
3508
+ coveredPaths.add(canonicalPath(searchPath));
2997
3509
  const files = await walkDir(searchPath);
2998
3510
  for (const file of files) {
2999
3511
  if (!adapter.matches(file))
@@ -3008,7 +3520,7 @@ async function scan() {
3008
3520
  }
3009
3521
  const discovered = discoverProjectPlanDirs();
3010
3522
  for (const { dir, agent } of discovered) {
3011
- const resolvedDir = resolve3(dir);
3523
+ const resolvedDir = canonicalPath(dir);
3012
3524
  if (coveredPaths.has(resolvedDir))
3013
3525
  continue;
3014
3526
  const adapter = adapters.find((a) => a.agent === agent);
@@ -3042,29 +3554,86 @@ function getAll() {
3042
3554
  function getIndexablePlans() {
3043
3555
  return getAll().filter(isIndexablePlan);
3044
3556
  }
3557
+ function getById(id) {
3558
+ return store.get(id);
3559
+ }
3560
+ function getPlanSourceAdapter(plan) {
3561
+ const sourceAdapter = plan.metadata.sourceAdapter;
3562
+ return typeof sourceAdapter === "string" && sourceAdapter.trim() ? sourceAdapter : undefined;
3563
+ }
3564
+ function findAdapterForPlan(plan) {
3565
+ const adapters = getActiveAdapters();
3566
+ const sourceAdapter = getPlanSourceAdapter(plan);
3567
+ return adapters.find((adapter) => adapter.agent === sourceAdapter) ?? adapters.find((adapter) => adapter.agent === plan.agent);
3568
+ }
3569
+ async function requestChanges(id, payload) {
3570
+ const plan = store.get(id);
3571
+ if (!plan)
3572
+ return false;
3573
+ const adapter = findAdapterForPlan(plan);
3574
+ if (!adapter?.requestChanges)
3575
+ return false;
3576
+ const ok = await adapter.requestChanges(plan, payload);
3577
+ if (ok) {
3578
+ const plannotator = typeof plan.metadata.plannotator === "object" && plan.metadata.plannotator !== null ? { ...plan.metadata.plannotator, lastWritebackStatus: "sent", lastWritebackAt: Date.now() } : undefined;
3579
+ if (plannotator)
3580
+ plan.metadata = { ...plan.metadata, plannotator };
3581
+ plan.updatedAt = new Date;
3582
+ notifyPlansChanged();
3583
+ }
3584
+ return ok;
3585
+ }
3586
+ function planBelongsToAdapter(plan, adapter) {
3587
+ if (plan.agent === adapter.agent)
3588
+ return true;
3589
+ return adapter.agent === "plannotator" && typeof plan.metadata.plannotator === "object" && plan.metadata.plannotator !== null;
3590
+ }
3591
+ function removePlansForPath(filePath, adapter) {
3592
+ const normalized = resolve4(filePath);
3593
+ const removed = [];
3594
+ for (const [id, plan] of store.entries()) {
3595
+ const planPath = resolve4(plan.filePath);
3596
+ const sessionPath = typeof plan.metadata.plannotator === "object" && plan.metadata.plannotator !== null ? plan.metadata.plannotator.sessionPath : undefined;
3597
+ if ((planPath === normalized || sessionPath === normalized) && (!adapter || planBelongsToAdapter(plan, adapter))) {
3598
+ removed.push(plan);
3599
+ store.delete(id);
3600
+ }
3601
+ }
3602
+ if (removed.length > 0)
3603
+ notifyPlansChanged();
3604
+ return removed;
3605
+ }
3045
3606
  async function rescanFile(filePath) {
3046
3607
  const adapters = getActiveAdapters();
3047
- const normalized = resolve3(filePath);
3608
+ const normalized = resolve4(filePath);
3609
+ const removedPlans = [];
3048
3610
  for (const adapter of adapters) {
3049
3611
  if (!adapter.matches(filePath))
3050
3612
  continue;
3051
- const discoveredDirs = discoverProjectPlanDirs().filter((d2) => d2.agent === adapter.agent).map((d2) => resolve3(d2.dir));
3613
+ const discoveredDirs = discoverProjectPlanDirs().filter((d2) => d2.agent === adapter.agent).map((d2) => resolve4(d2.dir));
3052
3614
  const allSearchPaths = [
3053
- ...adapter.getSearchPaths().map((sp) => resolve3(sp)),
3615
+ ...adapter.getSearchPaths().map((sp) => resolve4(sp)),
3054
3616
  ...discoveredDirs
3055
3617
  ];
3056
- const isInSearchPath = allSearchPaths.some((sp) => normalized.startsWith(sp + sep2) || normalized === sp);
3618
+ const isInSearchPath = allSearchPaths.some((sp) => normalized.startsWith(sp + sep3) || normalized === sp);
3057
3619
  if (!isInSearchPath)
3058
3620
  continue;
3059
- const plans = (await adapter.parse(filePath)).map(annotatePlanValueMetadata);
3621
+ const rawPlans = await adapter.parse(filePath);
3622
+ if (rawPlans.length === 0) {
3623
+ removedPlans.push(...removePlansForPath(filePath, adapter));
3624
+ continue;
3625
+ }
3626
+ const plans = rawPlans.map(annotatePlanValueMetadata);
3060
3627
  for (const plan of plans) {
3061
3628
  store.set(plan.id, plan);
3062
3629
  }
3063
3630
  notifyPlansChanged();
3064
3631
  return plans;
3065
3632
  }
3066
- const userPlansDir = resolve3(getUserPlansDir());
3067
- if (normalized.endsWith(".md") && (normalized.startsWith(userPlansDir + sep2) || normalized === userPlansDir)) {
3633
+ if (removedPlans.length > 0)
3634
+ return removedPlans;
3635
+ const userPlansDir = resolve4(getUserPlansDir());
3636
+ if (normalized.endsWith(".md") && (normalized.startsWith(userPlansDir + sep3) || normalized === userPlansDir)) {
3068
3637
  const plan = await parseGenericMarkdownPlan(filePath, { userCreated: true });
3069
3638
  if (plan) {
3070
3639
  store.set(plan.id, plan);
@@ -3075,8 +3644,8 @@ async function rescanFile(filePath) {
3075
3644
  if (normalized.endsWith(".md")) {
3076
3645
  const customDirs = getCustomPlanDirs();
3077
3646
  for (const dir of customDirs) {
3078
- const resolvedDir = resolve3(dir);
3079
- if (normalized.startsWith(resolvedDir + sep2) || normalized === resolvedDir) {
3647
+ const resolvedDir = resolve4(dir);
3648
+ if (normalized.startsWith(resolvedDir + sep3) || normalized === resolvedDir) {
3080
3649
  const plan = await parseGenericMarkdownPlan(filePath, {
3081
3650
  source: "custom-dir",
3082
3651
  customDir: dir
@@ -3092,8 +3661,8 @@ async function rescanFile(filePath) {
3092
3661
  return [];
3093
3662
  }
3094
3663
  // ../shared/src/services/watcher.ts
3095
- import { existsSync as existsSync5, watch } from "node:fs";
3096
- import { join as join9, resolve as resolve4 } from "node:path";
3664
+ import { existsSync as existsSync6, watch } from "node:fs";
3665
+ import { join as join10, resolve as resolve5 } from "node:path";
3097
3666
  var debounceTimer = null;
3098
3667
  var pendingFiles = new Set;
3099
3668
  var activeWatchers = [];
@@ -3111,13 +3680,13 @@ function closeAllWatchers() {
3111
3680
  activeWatchers.length = 0;
3112
3681
  }
3113
3682
  function watchDir(dir, matchFn, onChange) {
3114
- if (!existsSync5(dir))
3683
+ if (!existsSync6(dir))
3115
3684
  return;
3116
3685
  try {
3117
3686
  const watcher = watch(dir, { recursive: true }, async (_event, filename) => {
3118
3687
  if (!filename)
3119
3688
  return;
3120
- const fullPath = join9(dir, filename);
3689
+ const fullPath = join10(dir, filename);
3121
3690
  if (!matchFn(fullPath))
3122
3691
  return;
3123
3692
  pendingFiles.add(fullPath);
@@ -3150,23 +3719,23 @@ function setupWatchers(onChange) {
3150
3719
  const watchedPaths = new Set;
3151
3720
  for (const adapter of adapters) {
3152
3721
  for (const watchPath of adapter.getWatchPaths()) {
3153
- watchedPaths.add(resolve4(watchPath));
3722
+ watchedPaths.add(resolve5(watchPath));
3154
3723
  watchDir(watchPath, (f) => adapter.matches(f), onChange);
3155
3724
  }
3156
3725
  }
3157
3726
  const discovered = discoverProjectPlanDirs();
3158
3727
  for (const { dir, agent } of discovered) {
3159
- if (watchedPaths.has(resolve4(dir)))
3728
+ if (watchedPaths.has(resolve5(dir)))
3160
3729
  continue;
3161
3730
  const adapter = adapters.find((a) => a.agent === agent);
3162
3731
  if (!adapter)
3163
3732
  continue;
3164
- watchedPaths.add(resolve4(dir));
3733
+ watchedPaths.add(resolve5(dir));
3165
3734
  watchDir(dir, (f) => adapter.matches(f), onChange);
3166
3735
  }
3167
3736
  const customDirs = getCustomPlanDirs();
3168
3737
  for (const dir of customDirs) {
3169
- const resolvedCustom = resolve4(dir);
3738
+ const resolvedCustom = resolve5(dir);
3170
3739
  let overlaps = false;
3171
3740
  for (const watched of watchedPaths) {
3172
3741
  if (pathsOverlapFilesystemTree(resolvedCustom, watched)) {
@@ -3186,15 +3755,15 @@ import { request as httpsRequest } from "node:https";
3186
3755
  import { hostname as osHostname } from "node:os";
3187
3756
 
3188
3757
  // src/pid.ts
3189
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
3758
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
3190
3759
  import { hostname } from "node:os";
3191
- import { dirname, join as join10 } from "node:path";
3760
+ import { dirname as dirname3, join as join11 } from "node:path";
3192
3761
  function getPidPath() {
3193
- return join10(getConfigDir(), "daemon.pid");
3762
+ return join11(getConfigDir(), "daemon.pid");
3194
3763
  }
3195
3764
  function writePid() {
3196
3765
  const path = getPidPath();
3197
- mkdirSync2(dirname(path), { recursive: true });
3766
+ mkdirSync2(dirname3(path), { recursive: true });
3198
3767
  const info = {
3199
3768
  pid: process.pid,
3200
3769
  startedAtMs: Date.now(),
@@ -3204,7 +3773,7 @@ function writePid() {
3204
3773
  }
3205
3774
  function readPidInfo() {
3206
3775
  const path = getPidPath();
3207
- if (!existsSync6(path))
3776
+ if (!existsSync7(path))
3208
3777
  return null;
3209
3778
  const raw = readFileSync4(path, "utf-8").trim();
3210
3779
  const asNumber = Number(raw);
@@ -3356,6 +3925,7 @@ async function sendHeartbeat() {
3356
3925
  }
3357
3926
  async function sendShutdown() {
3358
3927
  try {
3928
+ getCloudConfig();
3359
3929
  cachedDeviceId ??= loadOrCreateDeviceId();
3360
3930
  await deleteDaemons([cachedDeviceId]);
3361
3931
  } catch {}
@@ -3376,6 +3946,7 @@ async function refreshToken(currentToken, convexUrl) {
3376
3946
  return null;
3377
3947
  return { token: body.token, expiresAt: body.expiresAt ?? 0 };
3378
3948
  }
3949
+ var REQUEST_TIMEOUT_MS2 = Number.parseInt(process.env.AGENDEX_HTTP_TIMEOUT_MS ?? "", 10) || 1e4;
3379
3950
  function requestText(urlString, options) {
3380
3951
  const url = new URL(urlString);
3381
3952
  const request = url.protocol === "https:" ? httpsRequest : httpRequest;
@@ -3383,11 +3954,12 @@ function requestText(urlString, options) {
3383
3954
  if (options.body) {
3384
3955
  headers["Content-Length"] = String(Buffer.byteLength(options.body));
3385
3956
  }
3386
- return new Promise((resolve5, reject) => {
3957
+ return new Promise((resolve6, reject) => {
3387
3958
  const req = request(url, {
3388
3959
  agent: false,
3389
3960
  headers,
3390
- method: options.method
3961
+ method: options.method,
3962
+ timeout: REQUEST_TIMEOUT_MS2
3391
3963
  }, (res) => {
3392
3964
  let body = "";
3393
3965
  res.setEncoding("utf8");
@@ -3395,7 +3967,7 @@ function requestText(urlString, options) {
3395
3967
  body += chunk;
3396
3968
  });
3397
3969
  res.on("end", () => {
3398
- resolve5({
3970
+ resolve6({
3399
3971
  status: res.statusCode ?? 0,
3400
3972
  body
3401
3973
  });
@@ -3403,12 +3975,71 @@ function requestText(urlString, options) {
3403
3975
  res.on("error", reject);
3404
3976
  });
3405
3977
  req.on("error", reject);
3978
+ req.on("timeout", () => {
3979
+ req.destroy(new Error(`Request to ${url.host} timed out after ${REQUEST_TIMEOUT_MS2}ms`));
3980
+ });
3406
3981
  if (options.body) {
3407
3982
  req.write(options.body);
3408
3983
  }
3409
3984
  req.end();
3410
3985
  });
3411
3986
  }
3987
+ function authHeaders(token, contentType = false) {
3988
+ return {
3989
+ Authorization: `Bearer ${token}`,
3990
+ Connection: "close",
3991
+ ...contentType && { "Content-Type": "application/json" }
3992
+ };
3993
+ }
3994
+ async function fetchPlannotatorWritebacks(limit = 10) {
3995
+ const { token, convexUrl } = getCloudConfig();
3996
+ cachedDeviceId ??= loadOrCreateDeviceId();
3997
+ const url = `${convexUrl}/api/cli/plannotator/writebacks?deviceId=${encodeURIComponent(cachedDeviceId)}&limit=${limit}`;
3998
+ let activeToken = token;
3999
+ let res = await requestText(url, {
4000
+ method: "GET",
4001
+ headers: authHeaders(activeToken)
4002
+ });
4003
+ if (res.status === 401) {
4004
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
4005
+ if (refreshed) {
4006
+ activeToken = refreshed;
4007
+ res = await requestText(url, {
4008
+ method: "GET",
4009
+ headers: authHeaders(activeToken)
4010
+ });
4011
+ }
4012
+ }
4013
+ if (res.status === 401)
4014
+ throw new AuthExpiredError;
4015
+ if (res.status < 200 || res.status >= 300)
4016
+ return [];
4017
+ const body = JSON.parse(res.body);
4018
+ return body.writebacks ?? [];
4019
+ }
4020
+ async function reportPlannotatorWriteback(writebackId, status, error) {
4021
+ const { token, convexUrl } = getCloudConfig();
4022
+ const url = `${convexUrl}/api/cli/plannotator/writebacks/report`;
4023
+ let activeToken = token;
4024
+ const body = JSON.stringify({ writebackId, status, error });
4025
+ let res = await requestText(url, {
4026
+ method: "POST",
4027
+ headers: authHeaders(activeToken, true),
4028
+ body
4029
+ });
4030
+ if (res.status === 401) {
4031
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
4032
+ if (refreshed) {
4033
+ activeToken = refreshed;
4034
+ res = await requestText(url, {
4035
+ method: "POST",
4036
+ headers: authHeaders(activeToken, true),
4037
+ body
4038
+ });
4039
+ }
4040
+ }
4041
+ return res.status >= 200 && res.status < 300;
4042
+ }
3412
4043
  async function fetchDevices() {
3413
4044
  const { token, convexUrl } = getCloudConfig();
3414
4045
  const url = `${convexUrl}/api/cli/devices`;
@@ -3509,6 +4140,7 @@ async function login(siteUrlOverride) {
3509
4140
  token: existing?.token,
3510
4141
  cloudToken: callback.token,
3511
4142
  convexUrl: callback.convexUrl,
4143
+ deviceId: existing?.deviceId,
3512
4144
  enabledAdapters: existing?.enabledAdapters ?? [],
3513
4145
  customPlanDirs: existing?.customPlanDirs ?? []
3514
4146
  };
@@ -3527,6 +4159,7 @@ function logout() {
3527
4159
  token: existing.token,
3528
4160
  cloudToken: undefined,
3529
4161
  convexUrl: undefined,
4162
+ deviceId: existing.deviceId,
3530
4163
  enabledAdapters: existing.enabledAdapters,
3531
4164
  customPlanDirs: existing.customPlanDirs
3532
4165
  };
@@ -3539,14 +4172,14 @@ async function startCallbackServer() {
3539
4172
  let timeout;
3540
4173
  let settle;
3541
4174
  let fail;
3542
- const result = new Promise((resolve5, reject) => {
3543
- settle = resolve5;
4175
+ const result = new Promise((resolve6, reject) => {
4176
+ settle = resolve6;
3544
4177
  fail = reject;
3545
4178
  });
3546
4179
  const finish = (value) => {
3547
4180
  if (!settle || !fail)
3548
4181
  return;
3549
- const resolve5 = settle;
4182
+ const resolve6 = settle;
3550
4183
  const reject = fail;
3551
4184
  settle = undefined;
3552
4185
  fail = undefined;
@@ -3563,7 +4196,7 @@ async function startCallbackServer() {
3563
4196
  reject(value);
3564
4197
  return;
3565
4198
  }
3566
- resolve5(value);
4199
+ resolve6(value);
3567
4200
  };
3568
4201
  server.on("request", (req, res) => {
3569
4202
  const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -3597,8 +4230,8 @@ async function startCallbackServer() {
3597
4230
  sockets.delete(socket);
3598
4231
  });
3599
4232
  });
3600
- await new Promise((resolve5, reject) => {
3601
- server.listen(0, "127.0.0.1", () => resolve5());
4233
+ await new Promise((resolve6, reject) => {
4234
+ server.listen(0, "127.0.0.1", () => resolve6());
3602
4235
  server.once("error", reject);
3603
4236
  });
3604
4237
  server.once("error", (error) => {
@@ -3678,19 +4311,77 @@ function spawnBrowser(command, args, options = {}) {
3678
4311
 
3679
4312
  // src/daemon.ts
3680
4313
  import { spawn as spawn2 } from "node:child_process";
3681
- import { resolve as resolve5 } from "node:path";
4314
+ import { resolve as resolve6 } from "node:path";
3682
4315
  import { fileURLToPath } from "node:url";
3683
4316
 
4317
+ // src/adapters.ts
4318
+ var PLANNOTATOR_ADAPTER_ID = "plannotator";
4319
+ function envFlag(name) {
4320
+ const value = process.env[name]?.trim().toLowerCase();
4321
+ if (!value)
4322
+ return;
4323
+ if (value === "1" || value === "true" || value === "yes" || value === "on")
4324
+ return true;
4325
+ if (value === "0" || value === "false" || value === "no" || value === "off")
4326
+ return false;
4327
+ return;
4328
+ }
4329
+ function shouldEnablePlannotatorSync(config) {
4330
+ const explicit = envFlag("AGENDEX_PLANNOTATOR_SYNC");
4331
+ if (explicit !== undefined)
4332
+ return explicit;
4333
+ return Boolean(config.cloudToken && config.convexUrl);
4334
+ }
4335
+ function resolveCliAdapterIds(config) {
4336
+ const ids = config.enabledAdapters.length > 0 ? [...config.enabledAdapters] : getDefaultAdapterIds();
4337
+ if (shouldEnablePlannotatorSync(config) && !ids.includes(PLANNOTATOR_ADAPTER_ID)) {
4338
+ ids.push(PLANNOTATOR_ADAPTER_ID);
4339
+ }
4340
+ return ids;
4341
+ }
4342
+
4343
+ // src/payload.ts
4344
+ var SYNC_METADATA_KEY = "agendexSync";
4345
+ function isRecord4(value) {
4346
+ return typeof value === "object" && value !== null;
4347
+ }
4348
+ function withSyncDeviceMetadata(metadata, deviceId) {
4349
+ if (!deviceId)
4350
+ return metadata;
4351
+ const existing = isRecord4(metadata[SYNC_METADATA_KEY]) ? metadata[SYNC_METADATA_KEY] : {};
4352
+ return {
4353
+ ...metadata,
4354
+ [SYNC_METADATA_KEY]: {
4355
+ ...existing,
4356
+ deviceId
4357
+ }
4358
+ };
4359
+ }
4360
+ function planToSyncPayload(plan, deviceId) {
4361
+ return {
4362
+ localPlanId: plan.id,
4363
+ agent: plan.agent,
4364
+ title: plan.title,
4365
+ content: plan.content,
4366
+ format: plan.format,
4367
+ filePath: plan.filePath,
4368
+ workspace: plan.workspace,
4369
+ metadata: withSyncDeviceMetadata(plan.metadata, deviceId),
4370
+ createdAt: plan.createdAt.getTime(),
4371
+ updatedAt: plan.updatedAt.getTime()
4372
+ };
4373
+ }
4374
+
3684
4375
  // src/sync-cache.ts
3685
4376
  import { createHash as createHash2 } from "node:crypto";
3686
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
3687
- import { join as join11 } from "node:path";
4377
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4378
+ import { join as join12 } from "node:path";
3688
4379
  function getCachePath() {
3689
- return join11(getConfigDir(), "sync-cache.json");
4380
+ return join12(getConfigDir(), "sync-cache.json");
3690
4381
  }
3691
4382
  function loadSyncCache() {
3692
4383
  const cachePath = getCachePath();
3693
- if (!existsSync7(cachePath))
4384
+ if (!existsSync8(cachePath))
3694
4385
  return {};
3695
4386
  try {
3696
4387
  const raw = JSON.parse(readFileSync5(cachePath, "utf-8"));
@@ -3703,7 +4394,7 @@ function loadSyncCache() {
3703
4394
  }
3704
4395
  function saveSyncCache(cache, options) {
3705
4396
  const dir = getConfigDir();
3706
- if (!existsSync7(dir))
4397
+ if (!existsSync8(dir))
3707
4398
  mkdirSync3(dir, { recursive: true });
3708
4399
  const cachePath = getCachePath();
3709
4400
  if (options?.replace) {
@@ -3729,32 +4420,71 @@ function computePayloadHash(payload) {
3729
4420
  return createHash2("sha256").update(canonical).digest("hex").slice(0, 20);
3730
4421
  }
3731
4422
 
4423
+ // src/writeback-delivery-cache.ts
4424
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4425
+ import { join as join13 } from "node:path";
4426
+ var MAX_PENDING_WRITEBACK_REPORTS = 1000;
4427
+ function getCachePath2() {
4428
+ return join13(getConfigDir(), "plannotator-writebacks-delivered.json");
4429
+ }
4430
+ function isPendingWritebackReportStatus(value) {
4431
+ return value === "sent" || value === "failed" || value === "expired";
4432
+ }
4433
+ function normalizeReports(input) {
4434
+ if (!Array.isArray(input))
4435
+ return [];
4436
+ const reports = new Map;
4437
+ for (const item of input) {
4438
+ if (typeof item === "string" && item.length > 0) {
4439
+ reports.set(item, "sent");
4440
+ continue;
4441
+ }
4442
+ if (item && typeof item === "object" && "id" in item && "status" in item && typeof item.id === "string" && item.id.length > 0 && isPendingWritebackReportStatus(item.status)) {
4443
+ reports.set(item.id, item.status);
4444
+ }
4445
+ }
4446
+ return [...reports.entries()].slice(-MAX_PENDING_WRITEBACK_REPORTS);
4447
+ }
4448
+ function loadPendingWritebackReports() {
4449
+ const cachePath = getCachePath2();
4450
+ if (!existsSync9(cachePath))
4451
+ return new Map;
4452
+ try {
4453
+ return new Map(normalizeReports(JSON.parse(readFileSync6(cachePath, "utf-8"))));
4454
+ } catch {
4455
+ return new Map;
4456
+ }
4457
+ }
4458
+ function savePendingWritebackReports(reports) {
4459
+ try {
4460
+ const dir = getConfigDir();
4461
+ if (!existsSync9(dir))
4462
+ mkdirSync4(dir, { recursive: true });
4463
+ const normalizedReports = normalizeReports([...reports].map(([id, status]) => ({ id, status })));
4464
+ writeFileSync4(getCachePath2(), JSON.stringify(normalizedReports.map(([id, status]) => ({ id, status }))));
4465
+ return true;
4466
+ } catch {
4467
+ return false;
4468
+ }
4469
+ }
4470
+
3732
4471
  // src/daemon.ts
3733
4472
  var MAX_RESTARTS = 5;
3734
4473
  var RESTART_WINDOW_MS = 60000;
3735
4474
  var RESTART_DELAY_MS = 5000;
3736
- function planToPayload(plan) {
3737
- return {
3738
- localPlanId: plan.id,
3739
- agent: plan.agent,
3740
- title: plan.title,
3741
- content: plan.content,
3742
- format: plan.format,
3743
- filePath: plan.filePath,
3744
- workspace: plan.workspace,
3745
- metadata: plan.metadata,
3746
- createdAt: plan.createdAt.getTime(),
3747
- updatedAt: plan.updatedAt.getTime()
3748
- };
3749
- }
4475
+ var PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS = 15000;
4476
+ var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4477
+ var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the request-changes payload.";
3750
4478
  async function runWorker() {
3751
4479
  const config = await loadOrInitConfig();
3752
- const adapters = resolveAdapters(config.enabledAdapters);
4480
+ const adapterIds = resolveCliAdapterIds(config);
4481
+ const adapters = resolveAdapters(adapterIds);
3753
4482
  setActiveAdapters(adapters);
3754
- console.log(`[agendex] daemon starting with ${config.enabledAdapters.length} adapters`);
4483
+ console.log(`[agendex] daemon starting with ${adapterIds.length} adapters`);
3755
4484
  await sendHeartbeat();
3756
4485
  const syncCache = loadSyncCache();
3757
4486
  const syncQueue = [];
4487
+ const pendingWritebackReports = loadPendingWritebackReports();
3758
4488
  let syncing = false;
3759
4489
  async function tryRefreshToken() {
3760
4490
  const cfg = loadConfig();
@@ -3819,6 +4549,89 @@ async function runWorker() {
3819
4549
  if (syncQueue.length > 0)
3820
4550
  processSyncQueue();
3821
4551
  }
4552
+ function persistPendingWritebackReports() {
4553
+ if (!savePendingWritebackReports(pendingWritebackReports)) {
4554
+ console.error("[agendex] failed to persist Plannotator write-back delivery cache");
4555
+ }
4556
+ }
4557
+ async function reportPendingWriteback(writebackId) {
4558
+ const status = pendingWritebackReports.get(writebackId);
4559
+ if (!status)
4560
+ return;
4561
+ const reported = await reportPlannotatorWriteback(writebackId, status, status === "expired" ? PLANNOTATOR_WRITEBACK_EXPIRED_ERROR : status === "failed" ? PLANNOTATOR_WRITEBACK_FAILED_ERROR : undefined);
4562
+ if (reported) {
4563
+ pendingWritebackReports.delete(writebackId);
4564
+ persistPendingWritebackReports();
4565
+ } else {
4566
+ console.error("[agendex] failed to report write-back status for", writebackId, "- will retry");
4567
+ }
4568
+ }
4569
+ async function handlePlannotatorWriteback(job) {
4570
+ if (pendingWritebackReports.has(job._id)) {
4571
+ await reportPendingWriteback(job._id);
4572
+ return;
4573
+ }
4574
+ if (job.expiresAt <= Date.now()) {
4575
+ pendingWritebackReports.set(job._id, "expired");
4576
+ persistPendingWritebackReports();
4577
+ await reportPendingWriteback(job._id);
4578
+ return;
4579
+ }
4580
+ let localPlan = getById(job.localPlanId);
4581
+ if (!localPlan) {
4582
+ await scan();
4583
+ localPlan = getById(job.localPlanId);
4584
+ }
4585
+ if (!localPlan) {
4586
+ if (job.deviceId) {
4587
+ pendingWritebackReports.set(job._id, "failed");
4588
+ persistPendingWritebackReports();
4589
+ await reportPendingWriteback(job._id);
4590
+ }
4591
+ return;
4592
+ }
4593
+ const ok = await requestChanges(job.localPlanId, {
4594
+ feedback: job.feedback,
4595
+ revisedContent: job.revisedContent,
4596
+ annotations: job.annotations,
4597
+ source: job.source,
4598
+ writebackId: job._id,
4599
+ requestedAt: Date.now()
4600
+ });
4601
+ if (ok) {
4602
+ const updatedPlan = getById(job.localPlanId);
4603
+ if (updatedPlan)
4604
+ syncQueue.push(planToSyncPayload(updatedPlan, config.deviceId));
4605
+ pendingWritebackReports.set(job._id, "sent");
4606
+ persistPendingWritebackReports();
4607
+ await reportPendingWriteback(job._id);
4608
+ processSyncQueue();
4609
+ return;
4610
+ }
4611
+ pendingWritebackReports.set(job._id, "failed");
4612
+ persistPendingWritebackReports();
4613
+ await reportPendingWriteback(job._id);
4614
+ }
4615
+ let pollingWritebacks = false;
4616
+ async function pollPlannotatorWritebacks() {
4617
+ if (pollingWritebacks)
4618
+ return;
4619
+ pollingWritebacks = true;
4620
+ try {
4621
+ const jobs = await fetchPlannotatorWritebacks();
4622
+ for (const job of jobs) {
4623
+ await handlePlannotatorWriteback(job);
4624
+ }
4625
+ } catch (err) {
4626
+ if (err instanceof Error && err.name === "AuthExpiredError") {
4627
+ console.error("[agendex] session expired. Run `agendex login` to re-authenticate.");
4628
+ } else {
4629
+ console.error("[agendex] Plannotator write-back polling failed:", err);
4630
+ }
4631
+ } finally {
4632
+ pollingWritebacks = false;
4633
+ }
4634
+ }
3822
4635
  setOnPlansChanged(() => {});
3823
4636
  console.log(`[agendex] initial scan...`);
3824
4637
  await scan();
@@ -3829,7 +4642,7 @@ async function runWorker() {
3829
4642
  let initialQueuedSyncable = 0;
3830
4643
  let initialQueuedLowValue = 0;
3831
4644
  for (const plan of plans) {
3832
- const payload = planToPayload(plan);
4645
+ const payload = planToSyncPayload(plan, config.deviceId);
3833
4646
  const hash = computePayloadHash(payload);
3834
4647
  if (syncCache[plan.id] === hash) {
3835
4648
  initialSkipped++;
@@ -3852,9 +4665,13 @@ async function runWorker() {
3852
4665
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
3853
4666
  await processSyncQueue();
3854
4667
  setInterval(() => void sendHeartbeat(), CLI_DAEMON_HEARTBEAT_INTERVAL_MS);
4668
+ if (shouldEnablePlannotatorSync(config)) {
4669
+ setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4670
+ pollPlannotatorWritebacks();
4671
+ }
3855
4672
  startWatching((changedPlans) => {
3856
4673
  for (const plan of changedPlans) {
3857
- syncQueue.push(planToPayload(plan));
4674
+ syncQueue.push(planToSyncPayload(plan, config.deviceId));
3858
4675
  }
3859
4676
  processSyncQueue();
3860
4677
  });
@@ -3884,17 +4701,17 @@ async function startSupervisor() {
3884
4701
  };
3885
4702
  process.on("SIGTERM", shutdown);
3886
4703
  process.on("SIGINT", shutdown);
3887
- const scriptPath = resolve5(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
4704
+ const scriptPath = resolve6(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
3888
4705
  const restartTimes = [];
3889
4706
  while (!stopping) {
3890
4707
  workerProc = spawn2(process.execPath, [scriptPath, "start", "--worker"], {
3891
4708
  stdio: ["ignore", "inherit", "inherit"]
3892
4709
  });
3893
- const exitCode = await new Promise((resolve6) => {
3894
- workerProc?.once("exit", (code) => resolve6(code));
4710
+ const exitCode = await new Promise((resolve7) => {
4711
+ workerProc?.once("exit", (code) => resolve7(code));
3895
4712
  workerProc?.once("error", (error) => {
3896
4713
  console.error("[agendex] failed to spawn worker:", error);
3897
- resolve6(1);
4714
+ resolve7(1);
3898
4715
  });
3899
4716
  });
3900
4717
  workerProc = null;
@@ -3917,23 +4734,10 @@ async function startSupervisor() {
3917
4734
  }
3918
4735
 
3919
4736
  // 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
- }
3934
4737
  async function syncAll(force = false) {
3935
4738
  const config = await loadOrInitConfig();
3936
- const adapters = resolveAdapters(config.enabledAdapters);
4739
+ const adapterIds = resolveCliAdapterIds(config);
4740
+ const adapters = resolveAdapters(adapterIds);
3937
4741
  setActiveAdapters(adapters);
3938
4742
  console.log(`[agendex] Scanning local plans...`);
3939
4743
  await scan();
@@ -3951,7 +4755,7 @@ async function syncAll(force = false) {
3951
4755
  let failed = 0;
3952
4756
  for (const plan of [...syncablePlans, ...lowValuePlans]) {
3953
4757
  activePlanIds.add(plan.id);
3954
- const payload = planToPayload2(plan);
4758
+ const payload = planToSyncPayload(plan, config.deviceId);
3955
4759
  const hash = computePayloadHash(payload);
3956
4760
  if (!force && cache[plan.id] === hash) {
3957
4761
  skipped++;
@@ -3981,14 +4785,20 @@ async function syncAll(force = false) {
3981
4785
  console.log(`[agendex] Sync complete: ${synced} synced${lowValueSuffix}, ${skipped} unchanged, ${failed} failed`);
3982
4786
  }
3983
4787
 
4788
+ // src/upgrade.ts
4789
+ import { spawn as spawn3, spawnSync } from "node:child_process";
4790
+ import { realpathSync as realpathSync2 } from "node:fs";
4791
+ import { dirname as dirname4, resolve as resolve7, sep as sep4 } from "node:path";
4792
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4793
+
3984
4794
  // src/version.ts
3985
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4795
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
3986
4796
  import { tmpdir } from "node:os";
3987
- import { join as join12 } from "node:path";
4797
+ import { join as join14 } from "node:path";
3988
4798
  // package.json
3989
4799
  var package_default = {
3990
4800
  name: "agendex-cli",
3991
- version: "0.14.0",
4801
+ version: "0.16.0",
3992
4802
  description: "Agendex CLI for login, sync, and daemon workflows",
3993
4803
  homepage: "https://github.com/Tyru5/Agendex#readme",
3994
4804
  repository: {
@@ -4032,14 +4842,14 @@ var package_default = {
4032
4842
 
4033
4843
  // src/version.ts
4034
4844
  var CLI_VERSION = package_default.version;
4035
- var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join12(tmpdir(), ".agendex-update-cache.json");
4845
+ var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join14(tmpdir(), ".agendex-update-cache.json");
4036
4846
  var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
4037
4847
  var UPDATE_URL = process.env.AGENDEX_UPDATE_URL ?? "https://registry.npmjs.org/agendex-cli/latest";
4038
4848
  function readCache(current) {
4039
4849
  try {
4040
- if (!existsSync8(CACHE_FILE))
4850
+ if (!existsSync10(CACHE_FILE))
4041
4851
  return null;
4042
- const { result, ts } = JSON.parse(readFileSync6(CACHE_FILE, "utf8"));
4852
+ const { result, ts } = JSON.parse(readFileSync7(CACHE_FILE, "utf8"));
4043
4853
  if (Date.now() - ts > CACHE_TTL_MS)
4044
4854
  return null;
4045
4855
  return normalizeResult(result, current);
@@ -4049,14 +4859,16 @@ function readCache(current) {
4049
4859
  }
4050
4860
  function writeCache(result) {
4051
4861
  try {
4052
- writeFileSync4(CACHE_FILE, JSON.stringify({ result, ts: Date.now() }));
4862
+ writeFileSync5(CACHE_FILE, JSON.stringify({ result, ts: Date.now() }));
4053
4863
  } catch {}
4054
4864
  }
4055
- async function checkForUpdate() {
4865
+ async function checkForUpdate(options = {}) {
4056
4866
  const current = CLI_VERSION;
4057
- const cached = readCache(current);
4058
- if (cached)
4059
- return cached;
4867
+ if (!options.forceRefresh) {
4868
+ const cached = readCache(current);
4869
+ if (cached)
4870
+ return cached;
4871
+ }
4060
4872
  try {
4061
4873
  const controller = new AbortController;
4062
4874
  const timeout = setTimeout(() => controller.abort(), 3000);
@@ -4064,21 +4876,27 @@ async function checkForUpdate() {
4064
4876
  signal: controller.signal
4065
4877
  }).finally(() => clearTimeout(timeout));
4066
4878
  if (!res.ok) {
4067
- return { updateAvailable: false, current, latest: current };
4879
+ return { checked: false, updateAvailable: false, current, latest: current };
4068
4880
  }
4069
4881
  const data = await res.json();
4070
4882
  const latest = data.version;
4071
- const result = { updateAvailable: isNewer(latest, current), current, latest };
4883
+ const result = {
4884
+ checked: true,
4885
+ updateAvailable: isNewer(latest, current),
4886
+ current,
4887
+ latest
4888
+ };
4072
4889
  writeCache(result);
4073
4890
  return result;
4074
4891
  } catch {
4075
- return { updateAvailable: false, current, latest: current };
4892
+ return { checked: false, updateAvailable: false, current, latest: current };
4076
4893
  }
4077
4894
  }
4078
4895
  function normalizeResult(result, current) {
4079
4896
  if (typeof result.latest !== "string")
4080
4897
  return null;
4081
4898
  return {
4899
+ checked: true,
4082
4900
  updateAvailable: isNewer(result.latest, current),
4083
4901
  current,
4084
4902
  latest: result.latest
@@ -4098,6 +4916,195 @@ function isNewer(latest, current) {
4098
4916
  return false;
4099
4917
  }
4100
4918
 
4919
+ // src/upgrade.ts
4920
+ var PACKAGE_NAME = "agendex-cli";
4921
+ var moduleDir = dirname4(fileURLToPath2(import.meta.url));
4922
+ function getPackageRoot() {
4923
+ try {
4924
+ return realpathSync2(resolve7(moduleDir, ".."));
4925
+ } catch {
4926
+ return resolve7(moduleDir, "..");
4927
+ }
4928
+ }
4929
+ function detectPackageManager(packageRoot) {
4930
+ const userAgent = process.env.npm_config_user_agent ?? "";
4931
+ const execpath = process.env.npm_execpath ?? "";
4932
+ if (userAgent.startsWith("bun/") || execpath.includes("bun"))
4933
+ return "bun";
4934
+ if (userAgent.startsWith("pnpm/") || execpath.includes("pnpm"))
4935
+ return "pnpm";
4936
+ if (userAgent.startsWith("yarn/") || execpath.includes("yarn"))
4937
+ return "yarn";
4938
+ const lower = packageRoot.toLowerCase();
4939
+ if (lower.includes(`${sep4}.bun${sep4}`) || lower.includes("/.bun/"))
4940
+ return "bun";
4941
+ if (lower.includes(`${sep4}pnpm${sep4}`) || lower.includes("/pnpm/"))
4942
+ return "pnpm";
4943
+ if (lower.includes(`${sep4}yarn${sep4}`) || lower.includes("/yarn/"))
4944
+ return "yarn";
4945
+ if (typeof globalThis.Bun !== "undefined" || process.versions.bun) {
4946
+ return "bun";
4947
+ }
4948
+ return "npm";
4949
+ }
4950
+ function readYarnVersion() {
4951
+ const result = spawnSync("yarn", ["--version"], {
4952
+ encoding: "utf8",
4953
+ shell: process.platform === "win32"
4954
+ });
4955
+ if (result.error || result.status !== 0)
4956
+ return null;
4957
+ return result.stdout.trim() || null;
4958
+ }
4959
+ function parseMajorVersion(version) {
4960
+ const match = version.match(/^(\d+)/);
4961
+ if (!match)
4962
+ return null;
4963
+ const major = Number(match[1]);
4964
+ return Number.isFinite(major) ? major : null;
4965
+ }
4966
+ function buildGlobalInstallCommand(pm) {
4967
+ const pkgSpec = `${PACKAGE_NAME}@latest`;
4968
+ switch (pm) {
4969
+ case "bun":
4970
+ return {
4971
+ supported: true,
4972
+ command: { bin: "bun", args: ["add", "-g", pkgSpec], display: `bun add -g ${pkgSpec}` }
4973
+ };
4974
+ case "pnpm":
4975
+ return {
4976
+ supported: true,
4977
+ command: {
4978
+ bin: "pnpm",
4979
+ args: ["add", "-g", pkgSpec],
4980
+ display: `pnpm add -g ${pkgSpec}`
4981
+ }
4982
+ };
4983
+ case "yarn": {
4984
+ const yarnVersion = readYarnVersion();
4985
+ const yarnMajorVersion = yarnVersion ? parseMajorVersion(yarnVersion) : null;
4986
+ if (yarnMajorVersion !== null && yarnMajorVersion >= 2) {
4987
+ return {
4988
+ supported: false,
4989
+ reason: `automatic upgrade with Yarn only supports Yarn Classic (v1); detected Yarn v${yarnVersion}.`,
4990
+ manualCommand: `npm install -g ${pkgSpec}`
4991
+ };
4992
+ }
4993
+ return {
4994
+ supported: true,
4995
+ command: {
4996
+ bin: "yarn",
4997
+ args: ["global", "add", pkgSpec],
4998
+ display: `yarn global add ${pkgSpec}`
4999
+ }
5000
+ };
5001
+ }
5002
+ default:
5003
+ return {
5004
+ supported: true,
5005
+ command: {
5006
+ bin: "npm",
5007
+ args: ["install", "-g", pkgSpec],
5008
+ display: `npm install -g ${pkgSpec}`
5009
+ }
5010
+ };
5011
+ }
5012
+ }
5013
+ function isLikelyGlobalInstall(packageRoot) {
5014
+ const normalized = packageRoot.replace(/\\/g, "/");
5015
+ if (!normalized.includes("/node_modules/")) {
5016
+ return false;
5017
+ }
5018
+ const globalMarkers = [
5019
+ "/lib/node_modules/",
5020
+ "/pnpm/global/",
5021
+ "/yarn/global/",
5022
+ "/.bun/install/global/",
5023
+ "/.bun/install/",
5024
+ "/.config/yarn/global/",
5025
+ "/AppData/Roaming/npm/",
5026
+ "/AppData/Local/Yarn/",
5027
+ "/AppData/Local/pnpm/"
5028
+ ];
5029
+ return globalMarkers.some((marker) => normalized.includes(marker));
5030
+ }
5031
+ async function runUpgrade(opts) {
5032
+ const packageRoot = getPackageRoot();
5033
+ const pm = detectPackageManager(packageRoot);
5034
+ const isGlobal = isLikelyGlobalInstall(packageRoot);
5035
+ if (!isGlobal) {
5036
+ process.stderr.write(`[agendex] this CLI appears to be running from a local checkout or linked install:
5037
+ ` + `[agendex] ${packageRoot}
5038
+ ` + `[agendex] automatic upgrade only supports global installs.
5039
+ ` + `[agendex] reinstall globally (e.g. \`npm install -g ${PACKAGE_NAME}@latest\`) or update the source repo manually.
5040
+ `);
5041
+ return 1;
5042
+ }
5043
+ const { checked, updateAvailable, current, latest } = await checkForUpdate({
5044
+ forceRefresh: true
5045
+ });
5046
+ if (checked && !updateAvailable && !opts.force) {
5047
+ process.stdout.write(`[agendex] already up to date (v${current})
5048
+ `);
5049
+ return 0;
5050
+ }
5051
+ if (!checked) {
5052
+ process.stderr.write(`[agendex] could not verify the latest version; attempting upgrade anyway...
5053
+ `);
5054
+ }
5055
+ const commandResult = buildGlobalInstallCommand(pm);
5056
+ if (!commandResult.supported) {
5057
+ process.stderr.write(`[agendex] ${commandResult.reason}
5058
+ `);
5059
+ process.stderr.write(`[agendex] install the latest CLI manually, e.g. \`${commandResult.manualCommand}\`.
5060
+ `);
5061
+ return 1;
5062
+ }
5063
+ const cmd = commandResult.command;
5064
+ if (checked && updateAvailable) {
5065
+ process.stdout.write(`[agendex] upgrading: v${current} → v${latest}
5066
+ `);
5067
+ } else if (opts.force) {
5068
+ process.stdout.write(`[agendex] reinstalling v${CLI_VERSION} (forced)
5069
+ `);
5070
+ }
5071
+ process.stdout.write(`[agendex] running: ${cmd.display}
5072
+ `);
5073
+ return await new Promise((resolveExit) => {
5074
+ const child = spawn3(cmd.bin, cmd.args, {
5075
+ stdio: "inherit",
5076
+ shell: process.platform === "win32",
5077
+ env: process.env
5078
+ });
5079
+ let didError = false;
5080
+ child.on("error", (err) => {
5081
+ didError = true;
5082
+ process.stderr.write(`[agendex] failed to run ${cmd.bin}: ${err instanceof Error ? err.message : String(err)}
5083
+ `);
5084
+ process.stderr.write(`[agendex] you can run it manually: ${cmd.display}
5085
+ `);
5086
+ resolveExit(1);
5087
+ });
5088
+ child.on("close", (code) => {
5089
+ if (didError)
5090
+ return;
5091
+ if (code === 0) {
5092
+ process.stdout.write(`[agendex] upgrade complete.
5093
+ `);
5094
+ process.stdout.write(`[agendex] note: if the daemon is running, restart it: \`agendex stop && agendex start\`
5095
+ `);
5096
+ resolveExit(0);
5097
+ return;
5098
+ }
5099
+ process.stderr.write(`[agendex] upgrade failed with exit code ${code ?? "unknown"}
5100
+ `);
5101
+ process.stderr.write(`[agendex] you can run it manually: ${cmd.display}
5102
+ `);
5103
+ resolveExit(code ?? 1);
5104
+ });
5105
+ });
5106
+ }
5107
+
4101
5108
  // src/web.ts
4102
5109
  async function openAgendexWeb(siteUrlOverride) {
4103
5110
  const base = siteUrlOverride ?? getDefaultSiteUrl();
@@ -4135,7 +5142,7 @@ function firstCommandToken(argv) {
4135
5142
  return;
4136
5143
  }
4137
5144
  var command = firstCommandToken(args) ?? "start";
4138
- var cliEntry = resolve6(process.argv[1] ?? fileURLToPath2(import.meta.url));
5145
+ var cliEntry = resolve8(process.argv[1] ?? fileURLToPath3(import.meta.url));
4139
5146
  async function main() {
4140
5147
  const isInternal = args.includes("--daemon") || args.includes("--worker");
4141
5148
  if (command === "--version" || command === "-v") {
@@ -4153,16 +5160,16 @@ async function main() {
4153
5160
  "add-dir",
4154
5161
  "remove-dir",
4155
5162
  "list-dirs",
5163
+ "upgrade",
4156
5164
  "help",
4157
5165
  "--help",
4158
5166
  "-h"
4159
5167
  ].includes(command);
4160
5168
  if (!isInternal && !isPassthrough) {
4161
- const { updateAvailable, current, latest } = await checkForUpdate();
4162
- if (updateAvailable) {
4163
- writeStderr(`[agendex] update required: v${current} → v${latest}`);
4164
- writeStderr(`[agendex] run: npm i -g agendex-cli`);
4165
- return 1;
5169
+ const { checked, updateAvailable, current, latest } = await checkForUpdate();
5170
+ if (checked && updateAvailable) {
5171
+ writeStderr(`[agendex] update available: v${current} → v${latest}`);
5172
+ writeStderr(`[agendex] run: agendex upgrade`);
4166
5173
  }
4167
5174
  }
4168
5175
  switch (command) {
@@ -4201,7 +5208,7 @@ async function main() {
4201
5208
  const daemonArgs = [cliEntry, "start", "--daemon"];
4202
5209
  if (devFlag)
4203
5210
  daemonArgs.push("--dev");
4204
- const child = spawn3(process.execPath, daemonArgs, {
5211
+ const child = spawn4(process.execPath, daemonArgs, {
4205
5212
  detached: true,
4206
5213
  stdio: "ignore",
4207
5214
  env: { ...process.env, ...devFlag ? { AGENDEX_DEV: "1" } : {} }
@@ -4335,7 +5342,7 @@ async function main() {
4335
5342
  return 1;
4336
5343
  }
4337
5344
  const resolved = resolveCustomPlanDirPath(dirPath);
4338
- if (!existsSync9(resolved)) {
5345
+ if (!existsSync11(resolved)) {
4339
5346
  writeStderr(`[agendex] path does not exist: ${resolved}`);
4340
5347
  return 1;
4341
5348
  }
@@ -4354,7 +5361,7 @@ async function main() {
4354
5361
  const { request } = await import("node:http");
4355
5362
  const body = JSON.stringify({ path: resolved });
4356
5363
  try {
4357
- const res = await new Promise((resolve7, reject) => {
5364
+ const res = await new Promise((resolve9, reject) => {
4358
5365
  const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
4359
5366
  method: "POST",
4360
5367
  headers: {
@@ -4368,7 +5375,7 @@ async function main() {
4368
5375
  res2.on("data", (chunk) => {
4369
5376
  data += chunk;
4370
5377
  });
4371
- res2.on("end", () => resolve7({ status: res2.statusCode ?? 0, body: data }));
5378
+ res2.on("end", () => resolve9({ status: res2.statusCode ?? 0, body: data }));
4372
5379
  res2.on("error", reject);
4373
5380
  });
4374
5381
  req.on("error", reject);
@@ -4496,6 +5503,9 @@ async function main() {
4496
5503
  }
4497
5504
  return 0;
4498
5505
  }
5506
+ case "upgrade": {
5507
+ return await runUpgrade({ force: args.includes("--force") });
5508
+ }
4499
5509
  case "help":
4500
5510
  case "--help":
4501
5511
  case "-h": {
@@ -4522,6 +5532,8 @@ Usage:
4522
5532
  agendex cleanup Interactively remove cloud daemons
4523
5533
  agendex cleanup --stale Auto-remove all stale daemons
4524
5534
  agendex status Show current config state + daemon status
5535
+ agendex upgrade Upgrade the globally installed CLI to the latest version
5536
+ agendex upgrade --force Reinstall latest even if already up to date
4525
5537
  agendex help Show this help message
4526
5538
  agendex --version Print CLI version
4527
5539
  agendex -v Print CLI version
@@ -4550,13 +5562,13 @@ function flushStream(stream) {
4550
5562
  if (stream.destroyed || !stream.writable) {
4551
5563
  return Promise.resolve();
4552
5564
  }
4553
- return new Promise((resolve7, reject) => {
5565
+ return new Promise((resolve9, reject) => {
4554
5566
  stream.write("", (error) => {
4555
5567
  if (error) {
4556
5568
  reject(error);
4557
5569
  return;
4558
5570
  }
4559
- resolve7();
5571
+ resolve9();
4560
5572
  });
4561
5573
  });
4562
5574
  }