@wspc/cli 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command66 } from "commander";
4
+ import { Command as Command73 } from "commander";
5
5
  import { realpathSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
 
@@ -930,6 +930,20 @@ var eventIcsDownload = (options) => (options.client ?? client).get({
930
930
  url: "/calendar/events/{filename}",
931
931
  ...options
932
932
  });
933
+ var driveLibraryList = (options) => (options?.client ?? client).get({
934
+ security: [{ scheme: "bearer", type: "http" }],
935
+ url: "/drive/libraries",
936
+ ...options
937
+ });
938
+ var driveLibraryCreate = (options) => (options?.client ?? client).post({
939
+ security: [{ scheme: "bearer", type: "http" }],
940
+ url: "/drive/libraries",
941
+ ...options,
942
+ headers: {
943
+ "Content-Type": "application/json",
944
+ ...options?.headers
945
+ }
946
+ });
933
947
  var driveFileDelete = (options) => (options.client ?? client).post({
934
948
  security: [{ scheme: "bearer", type: "http" }],
935
949
  url: "/drive/libraries/{id}/files/delete",
@@ -939,11 +953,29 @@ var driveFileDelete = (options) => (options.client ?? client).post({
939
953
  ...options.headers
940
954
  }
941
955
  });
956
+ var driveLibraryDelete = (options) => (options.client ?? client).delete({
957
+ security: [{ scheme: "bearer", type: "http" }],
958
+ url: "/drive/libraries/{id}",
959
+ ...options,
960
+ headers: {
961
+ "Content-Type": "application/json",
962
+ ...options.headers
963
+ }
964
+ });
942
965
  var driveLibraryGet = (options) => (options.client ?? client).get({
943
966
  security: [{ scheme: "bearer", type: "http" }],
944
967
  url: "/drive/libraries/{id}",
945
968
  ...options
946
969
  });
970
+ var driveLibraryUpdate = (options) => (options.client ?? client).patch({
971
+ security: [{ scheme: "bearer", type: "http" }],
972
+ url: "/drive/libraries/{id}",
973
+ ...options,
974
+ headers: {
975
+ "Content-Type": "application/json",
976
+ ...options.headers
977
+ }
978
+ });
947
979
  var driveManifestGet = (options) => (options.client ?? client).get({
948
980
  security: [{ scheme: "bearer", type: "http" }],
949
981
  url: "/drive/libraries/{id}/manifest",
@@ -1432,9 +1464,9 @@ function createConsistencyFetch(opts) {
1432
1464
  }
1433
1465
 
1434
1466
  // src/version.ts
1435
- var VERSION = "0.1.1";
1436
- var SPEC_SHA = "51d0418e";
1437
- var SPEC_FETCHED_AT = "2026-06-21T12:30:00.342Z";
1467
+ var VERSION = "0.1.3";
1468
+ var SPEC_SHA = "e0bbc1e3";
1469
+ var SPEC_FETCHED_AT = "2026-06-22T01:23:52.693Z";
1438
1470
  var API_BASE = "https://api.wspc.ai";
1439
1471
 
1440
1472
  // src/index.ts
@@ -1588,8 +1620,19 @@ async function loadAuthedFetch(opts = {}) {
1588
1620
  const { fetch: fetch2, baseUrl } = await loadClientParts(opts);
1589
1621
  return { fetch: fetch2, baseUrl };
1590
1622
  }
1623
+ async function loadRealtimeAuthHeaders(opts = {}) {
1624
+ const { baseUrl, authInterceptor } = await loadClientParts(opts);
1625
+ const verifyUrl = new URL(opts.verifyPath ?? "/auth/me", `${baseUrl.replace(/\/$/, "")}/`);
1626
+ const verifyResponse = await authInterceptor.execute(new Request(verifyUrl));
1627
+ if (!verifyResponse.ok) {
1628
+ throw new Error(`realtime auth failed: HTTP ${verifyResponse.status}`);
1629
+ }
1630
+ const request = await authInterceptor.onRequest(new Request(baseUrl));
1631
+ return { baseUrl, headers: request.headers };
1632
+ }
1591
1633
  async function loadSdkClientWithAuthedFetch(opts = {}) {
1592
- return loadClientParts(opts);
1634
+ const { _rawClient, fetch: fetch2, baseUrl } = await loadClientParts(opts);
1635
+ return { _rawClient, fetch: fetch2, baseUrl };
1593
1636
  }
1594
1637
  async function loadClientParts(opts = {}) {
1595
1638
  const store = opts.store ?? new ConfigStore();
@@ -1609,7 +1652,7 @@ async function loadClientParts(opts = {}) {
1609
1652
  fetch: authedFetch
1610
1653
  })
1611
1654
  );
1612
- return { _rawClient: rawClient, fetch: authedFetch, baseUrl: resolved.apiBase };
1655
+ return { _rawClient: rawClient, fetch: authedFetch, baseUrl: resolved.apiBase, authInterceptor: interceptor };
1613
1656
  }
1614
1657
 
1615
1658
  // src/handwritten/output/primitives.ts
@@ -1835,22 +1878,24 @@ function render(ctx, data) {
1835
1878
  renderPaginationFooter(data);
1836
1879
  }
1837
1880
  function renderPaginationFooter(data) {
1838
- if (data === null || typeof data !== "object") return;
1839
- const d = data;
1840
- if (typeof d.next_cursor === "string" && d.next_cursor.length > 0) {
1841
- process.stdout.write(dim(` \u2026 more results \u2014 re-run with --cursor ${d.next_cursor}`) + "\n");
1842
- }
1843
- const id = typeof d.id === "string" ? d.id : "<id>";
1844
- if (typeof d.children_next_cursor === "string" && d.children_next_cursor.length > 0) {
1881
+ if (!isRecord(data)) return;
1882
+ const nextCursor = getString(data, "next_cursor");
1883
+ if (nextCursor !== void 0 && nextCursor.length > 0) {
1884
+ process.stdout.write(dim(` \u2026 more results \u2014 re-run with --cursor ${nextCursor}`) + "\n");
1885
+ }
1886
+ const id = getString(data, "id") ?? "<id>";
1887
+ const childrenNextCursor = getString(data, "children_next_cursor");
1888
+ if (childrenNextCursor !== void 0 && childrenNextCursor.length > 0) {
1845
1889
  process.stdout.write(dim(` \u2026 more children \u2014 wspc todo ls --parent ${id}`) + "\n");
1846
1890
  }
1847
- if (typeof d.comments_next_cursor === "string" && d.comments_next_cursor.length > 0) {
1891
+ const commentsNextCursor = getString(data, "comments_next_cursor");
1892
+ if (commentsNextCursor !== void 0 && commentsNextCursor.length > 0) {
1848
1893
  process.stdout.write(dim(` \u2026 more comments \u2014 wspc todo comment ls ${id}`) + "\n");
1849
1894
  }
1850
1895
  }
1851
1896
  function drillDataPath(data, dataPath) {
1852
1897
  if (!dataPath) return data;
1853
- if (data === null || typeof data !== "object") return data;
1898
+ if (!isRecord(data)) return data;
1854
1899
  const value = data[dataPath];
1855
1900
  return value === void 0 ? data : value;
1856
1901
  }
@@ -1878,9 +1923,9 @@ function renderGeneric(data, hints) {
1878
1923
  }
1879
1924
  function detectShape(data) {
1880
1925
  if (Array.isArray(data)) return "list";
1881
- if (typeof data === "object" && data !== null) {
1926
+ if (isRecord(data)) {
1882
1927
  const keys = Object.keys(data);
1883
- const arrayKeys = keys.filter((k) => Array.isArray(data[k]));
1928
+ const arrayKeys = keys.filter((k) => getArray(data, k) !== void 0);
1884
1929
  if (arrayKeys.length === 1) return "list";
1885
1930
  return "object";
1886
1931
  }
@@ -1888,9 +1933,10 @@ function detectShape(data) {
1888
1933
  }
1889
1934
  function extractItems(data) {
1890
1935
  if (Array.isArray(data)) return data;
1891
- if (typeof data === "object" && data !== null) {
1892
- for (const v of Object.values(data)) {
1893
- if (Array.isArray(v)) return v;
1936
+ if (isRecord(data)) {
1937
+ for (const key of Object.keys(data)) {
1938
+ const value = getArray(data, key);
1939
+ if (value !== void 0) return value;
1894
1940
  }
1895
1941
  }
1896
1942
  return [];
@@ -1901,13 +1947,13 @@ function renderList(data, hints) {
1901
1947
  process.stdout.write(dim(" " + (hints?.emptyMessage ?? "no items")) + "\n");
1902
1948
  return;
1903
1949
  }
1904
- const first = items[0];
1950
+ const first = isRecord(items[0]) ? items[0] : {};
1905
1951
  const columns = pickColumns(first, hints?.columns);
1906
1952
  const format = hints?.format ?? {};
1907
1953
  const headers = columns.map((c) => c.toUpperCase());
1908
1954
  const rows = items.map(
1909
1955
  (item) => columns.map(
1910
- (col) => formatCell(item[col], format[col], hints?.enumColorMap?.[col])
1956
+ (col) => formatCell(isRecord(item) ? item[col] : void 0, format[col], hints?.enumColorMap?.[col])
1911
1957
  )
1912
1958
  );
1913
1959
  process.stdout.write(table(headers, rows));
@@ -1925,7 +1971,7 @@ function pickColumns(first, hint) {
1925
1971
  return ordered.slice(0, 5);
1926
1972
  }
1927
1973
  function renderObject(data, hints) {
1928
- if (typeof data !== "object" || data === null) {
1974
+ if (!isRecord(data)) {
1929
1975
  renderScalar(data);
1930
1976
  return;
1931
1977
  }
@@ -1934,7 +1980,7 @@ function renderObject(data, hints) {
1934
1980
  if (topKeys.length === 1) {
1935
1981
  const onlyKey = topKeys[0];
1936
1982
  const inner = obj[onlyKey];
1937
- if (inner && typeof inner === "object" && !Array.isArray(inner)) {
1983
+ if (isRecord(inner)) {
1938
1984
  obj = inner;
1939
1985
  }
1940
1986
  }
@@ -1945,7 +1991,10 @@ function renderObject(data, hints) {
1945
1991
  }
1946
1992
  const format = hints?.format ?? {};
1947
1993
  const arrayFields = hints?.fields ? [] : Object.keys(obj).filter(
1948
- (k) => Array.isArray(obj[k]) && obj[k].length > 0
1994
+ (k) => {
1995
+ const value = getArray(obj, k);
1996
+ return value !== void 0 && value.length > 0;
1997
+ }
1949
1998
  );
1950
1999
  const formatted = fields.map((f) => [
1951
2000
  f,
@@ -1974,7 +2023,8 @@ function renderObject(data, hints) {
1974
2023
  for (const f of arrayFields) {
1975
2024
  const uncapped = f === "children" || f === "comments";
1976
2025
  const max = uncapped ? Number.POSITIVE_INFINITY : ARRAY_FIELD_MAX_ITEMS;
1977
- renderArrayField(f, obj[f], labelWidth, max);
2026
+ const items = getArray(obj, f);
2027
+ if (items !== void 0) renderArrayField(f, items, labelWidth, max);
1978
2028
  }
1979
2029
  const hadAbove = inlineFinal.length > 0 || arrayFields.length > 0;
1980
2030
  blocks.forEach(([f, value], i) => {
@@ -2026,28 +2076,31 @@ function formatArrayItem(item) {
2026
2076
  return JSON.stringify(item);
2027
2077
  }
2028
2078
  function formatTodoLike(item) {
2029
- if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2030
- const rec = item;
2031
- if (typeof rec.id !== "string" || typeof rec.title !== "string") return null;
2032
- const id = idShort(rec.id);
2033
- const status = typeof rec.status === "string" ? statusBadge(rec.status) : "";
2034
- return status ? `${id} ${status} ${rec.title}` : `${id} ${rec.title}`;
2079
+ if (!isRecord(item)) return null;
2080
+ const idValue = getString(item, "id");
2081
+ const title = getString(item, "title");
2082
+ if (idValue === void 0 || title === void 0) return null;
2083
+ const status = getString(item, "status");
2084
+ const id = idShort(idValue);
2085
+ const statusText = status === void 0 ? "" : statusBadge(status);
2086
+ return statusText ? `${id} ${statusText} ${title}` : `${id} ${title}`;
2035
2087
  }
2036
2088
  function formatCommentLike(item) {
2037
- if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2038
- const rec = item;
2039
- if (typeof rec.id !== "string" || typeof rec.content !== "string") return null;
2040
- const id = idShort(rec.id);
2041
- const when = rec.created_at !== void 0 ? `${relativeTime(rec.created_at)} ` : "";
2042
- const snippet = truncate(rec.content, 60);
2089
+ if (!isRecord(item)) return null;
2090
+ const idValue = getString(item, "id");
2091
+ const content = getString(item, "content");
2092
+ if (idValue === void 0 || content === void 0) return null;
2093
+ const id = idShort(idValue);
2094
+ const when = item.created_at !== void 0 ? `${relativeTime(item.created_at)} ` : "";
2095
+ const snippet = truncate(content, 60);
2043
2096
  return `${id} ${when}${snippet}`;
2044
2097
  }
2045
2098
  function formatAttendeeLike(item) {
2046
- if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2047
- const rec = item;
2048
- const email = typeof rec.email === "string" ? rec.email : null;
2099
+ if (!isRecord(item)) return null;
2100
+ const email = getString(item, "email");
2049
2101
  if (!email) return null;
2050
- const name = typeof rec.display_name === "string" && rec.display_name.length > 0 ? rec.display_name : null;
2102
+ const displayName = getString(item, "display_name");
2103
+ const name = displayName !== void 0 && displayName.length > 0 ? displayName : void 0;
2051
2104
  return name ? `${name} <${email}>` : `<${email}>`;
2052
2105
  }
2053
2106
  function renderScalar(data) {
@@ -2068,6 +2121,17 @@ function renderScalar(data) {
2068
2121
  function isScalar(v) {
2069
2122
  return v === null || typeof v !== "object" && typeof v !== "function";
2070
2123
  }
2124
+ function isRecord(value) {
2125
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2126
+ }
2127
+ function getString(record, key) {
2128
+ const value = record[key];
2129
+ return typeof value === "string" ? value : void 0;
2130
+ }
2131
+ function getArray(record, key) {
2132
+ const value = record[key];
2133
+ return Array.isArray(value) ? value : void 0;
2134
+ }
2071
2135
  function formatCell(value, fmt, colorMap, opts) {
2072
2136
  if (fmt !== "enum-badge" && (value === void 0 || value === null)) return dim("\u2014");
2073
2137
  switch (fmt) {
@@ -2697,9 +2761,174 @@ var eventIcsDownloadCommand = new Command21("ics").description("Download event a
2697
2761
  render({ kind: "event_ics_download", display: { "shape": "raw" } }, result.data);
2698
2762
  });
2699
2763
 
2700
- // src/generated/cli/alias/add.ts
2764
+ // src/generated/cli/drive/library/add.ts
2701
2765
  import { Command as Command22 } from "commander";
2702
- var emailAliasCreateCommand = new Command22("add").description("Create a receiving alias").argument("<email>", "email").action(async (email, opts) => {
2766
+ var driveLibraryCreateCommand = new Command22("add").description("Create a drive library").argument("<name>", "name").action(async (name, opts) => {
2767
+ const client2 = await loadSdkClient();
2768
+ const result = await driveLibraryCreate({
2769
+ client: client2._rawClient,
2770
+ body: {
2771
+ name
2772
+ }
2773
+ });
2774
+ if (result.error || !result.response?.ok) {
2775
+ process.stderr.write(
2776
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2777
+ `
2778
+ );
2779
+ process.exitCode = 1;
2780
+ return;
2781
+ }
2782
+ render({ kind: "drive_library_create", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
2783
+ });
2784
+
2785
+ // src/generated/cli/drive/library/ls.ts
2786
+ import { Command as Command23 } from "commander";
2787
+ var driveLibraryListCommand = new Command23("ls").description("List drive libraries").option("--limit <value>", "limit").option("--cursor <value>", "cursor").option("--include-deleted <value>", "include_deleted").action(async (opts) => {
2788
+ const client2 = await loadSdkClient();
2789
+ const result = await driveLibraryList({
2790
+ client: client2._rawClient,
2791
+ query: {
2792
+ limit: opts.limit,
2793
+ cursor: opts.cursor,
2794
+ include_deleted: opts.includeDeleted
2795
+ }
2796
+ });
2797
+ if (result.error || !result.response?.ok) {
2798
+ process.stderr.write(
2799
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2800
+ `
2801
+ );
2802
+ process.exitCode = 1;
2803
+ return;
2804
+ }
2805
+ render({ kind: "drive_library_list", display: { "shape": "list", "dataPath": "libraries", "columns": ["id", "name", "file_count", "storage_bytes", "updated_at"], "emptyMessage": "no drive libraries" } }, result.data);
2806
+ });
2807
+
2808
+ // src/generated/cli/drive/file/rm.ts
2809
+ import { Command as Command24 } from "commander";
2810
+ var driveFileDeleteCommand = new Command24("rm").description("Delete a drive file").argument("<id>", "id").argument("<path>", "path").option("--expected-entry-version <value>", "expected_entry_version").action(async (id, path, opts) => {
2811
+ const client2 = await loadSdkClient();
2812
+ const result = await driveFileDelete({
2813
+ client: client2._rawClient,
2814
+ path: {
2815
+ id
2816
+ },
2817
+ body: {
2818
+ path,
2819
+ expected_entry_version: opts.expectedEntryVersion
2820
+ }
2821
+ });
2822
+ if (result.error || !result.response?.ok) {
2823
+ process.stderr.write(
2824
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2825
+ `
2826
+ );
2827
+ process.exitCode = 1;
2828
+ return;
2829
+ }
2830
+ render({ kind: "drive_file_delete", display: { "shape": "object", "dataPath": "entry", "columns": ["path", "entry_version", "deleted_at"] } }, result.data);
2831
+ });
2832
+
2833
+ // src/generated/cli/drive/library/rm.ts
2834
+ import { Command as Command25 } from "commander";
2835
+ var driveLibraryDeleteCommand = new Command25("rm").description("Delete a drive library").argument("<id>", "id").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
2836
+ const client2 = await loadSdkClient();
2837
+ const result = await driveLibraryDelete({
2838
+ client: client2._rawClient,
2839
+ path: {
2840
+ id
2841
+ },
2842
+ body: {
2843
+ expected_version: opts.expectedVersion
2844
+ }
2845
+ });
2846
+ if (result.error || !result.response?.ok) {
2847
+ process.stderr.write(
2848
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2849
+ `
2850
+ );
2851
+ process.exitCode = 1;
2852
+ return;
2853
+ }
2854
+ render({ kind: "drive_library_delete", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
2855
+ });
2856
+
2857
+ // src/generated/cli/drive/library/show.ts
2858
+ import { Command as Command26 } from "commander";
2859
+ var driveLibraryGetCommand = new Command26("show").description("Get a drive library").argument("<id>", "id").action(async (id, opts) => {
2860
+ const client2 = await loadSdkClient();
2861
+ const result = await driveLibraryGet({
2862
+ client: client2._rawClient,
2863
+ path: {
2864
+ id
2865
+ }
2866
+ });
2867
+ if (result.error || !result.response?.ok) {
2868
+ process.stderr.write(
2869
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2870
+ `
2871
+ );
2872
+ process.exitCode = 1;
2873
+ return;
2874
+ }
2875
+ render({ kind: "drive_library_get", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
2876
+ });
2877
+
2878
+ // src/generated/cli/drive/library/update.ts
2879
+ import { Command as Command27 } from "commander";
2880
+ var driveLibraryUpdateCommand = new Command27("update").description("Update a drive library").argument("<id>", "id").option("--name <value>", "name").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
2881
+ const client2 = await loadSdkClient();
2882
+ const result = await driveLibraryUpdate({
2883
+ client: client2._rawClient,
2884
+ path: {
2885
+ id
2886
+ },
2887
+ body: {
2888
+ name: opts.name,
2889
+ expected_version: opts.expectedVersion
2890
+ }
2891
+ });
2892
+ if (result.error || !result.response?.ok) {
2893
+ process.stderr.write(
2894
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2895
+ `
2896
+ );
2897
+ process.exitCode = 1;
2898
+ return;
2899
+ }
2900
+ render({ kind: "drive_library_update", display: { "shape": "object", "columns": ["id", "name", "version", "file_count", "storage_bytes", "updated_at"] } }, result.data);
2901
+ });
2902
+
2903
+ // src/generated/cli/drive/manifest/get.ts
2904
+ import { Command as Command28 } from "commander";
2905
+ var driveManifestGetCommand = new Command28("get").description("Get a drive library manifest").argument("<id>", "id").option("--limit <value>", "limit").option("--cursor <value>", "cursor").option("--include-deleted <value>", "include_deleted").action(async (id, opts) => {
2906
+ const client2 = await loadSdkClient();
2907
+ const result = await driveManifestGet({
2908
+ client: client2._rawClient,
2909
+ path: {
2910
+ id
2911
+ },
2912
+ query: {
2913
+ limit: opts.limit,
2914
+ cursor: opts.cursor,
2915
+ include_deleted: opts.includeDeleted
2916
+ }
2917
+ });
2918
+ if (result.error || !result.response?.ok) {
2919
+ process.stderr.write(
2920
+ `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
2921
+ `
2922
+ );
2923
+ process.exitCode = 1;
2924
+ return;
2925
+ }
2926
+ render({ kind: "drive_manifest_get", display: { "shape": "list", "dataPath": "entries", "columns": ["path", "entry_version", "size_bytes", "updated_at"], "emptyMessage": "no drive files" } }, result.data);
2927
+ });
2928
+
2929
+ // src/generated/cli/alias/add.ts
2930
+ import { Command as Command29 } from "commander";
2931
+ var emailAliasCreateCommand = new Command29("add").description("Create a receiving alias").argument("<email>", "email").action(async (email, opts) => {
2703
2932
  const client2 = await loadSdkClient();
2704
2933
  const result = await emailAliasCreate({
2705
2934
  client: client2._rawClient,
@@ -2719,8 +2948,8 @@ var emailAliasCreateCommand = new Command22("add").description("Create a receivi
2719
2948
  });
2720
2949
 
2721
2950
  // src/generated/cli/alias/ls.ts
2722
- import { Command as Command23 } from "commander";
2723
- var emailAliasListCommand = new Command23("ls").description("List the caller's aliases").option("--include-deleted <value>", "When `true`, include soft-deleted aliases (with `deleted_at` set) alongside active ones. Defaults to `false`.").action(async (opts) => {
2951
+ import { Command as Command30 } from "commander";
2952
+ var emailAliasListCommand = new Command30("ls").description("List the caller's aliases").option("--include-deleted <value>", "When `true`, include soft-deleted aliases (with `deleted_at` set) alongside active ones. Defaults to `false`.").action(async (opts) => {
2724
2953
  const client2 = await loadSdkClient();
2725
2954
  const result = await emailAliasList({
2726
2955
  client: client2._rawClient,
@@ -2740,8 +2969,8 @@ var emailAliasListCommand = new Command23("ls").description("List the caller's a
2740
2969
  });
2741
2970
 
2742
2971
  // src/generated/cli/domain/add.ts
2743
- import { Command as Command24 } from "commander";
2744
- var emailDomainCreateCommand = new Command24("add").description("Register a custom email domain").argument("<domain>", "domain").action(async (domain, opts) => {
2972
+ import { Command as Command31 } from "commander";
2973
+ var emailDomainCreateCommand = new Command31("add").description("Register a custom email domain").argument("<domain>", "domain").action(async (domain, opts) => {
2745
2974
  const client2 = await loadSdkClient();
2746
2975
  const result = await emailDomainCreate({
2747
2976
  client: client2._rawClient,
@@ -2761,8 +2990,8 @@ var emailDomainCreateCommand = new Command24("add").description("Register a cust
2761
2990
  });
2762
2991
 
2763
2992
  // src/generated/cli/domain/ls.ts
2764
- import { Command as Command25 } from "commander";
2765
- var emailDomainListCommand = new Command25("ls").description("List cached custom domains").action(async (opts) => {
2993
+ import { Command as Command32 } from "commander";
2994
+ var emailDomainListCommand = new Command32("ls").description("List cached custom domains").action(async (opts) => {
2766
2995
  const client2 = await loadSdkClient();
2767
2996
  const result = await emailDomainList({
2768
2997
  client: client2._rawClient
@@ -2779,8 +3008,8 @@ var emailDomainListCommand = new Command25("ls").description("List cached custom
2779
3008
  });
2780
3009
 
2781
3010
  // src/generated/cli/alias/rm.ts
2782
- import { Command as Command26 } from "commander";
2783
- var emailAliasDeleteCommand = new Command26("rm").description("Soft-delete an alias").argument("<email>", "email").action(async (email, opts) => {
3011
+ import { Command as Command33 } from "commander";
3012
+ var emailAliasDeleteCommand = new Command33("rm").description("Soft-delete an alias").argument("<email>", "email").action(async (email, opts) => {
2784
3013
  const client2 = await loadSdkClient();
2785
3014
  const result = await emailAliasDelete({
2786
3015
  client: client2._rawClient,
@@ -2800,8 +3029,8 @@ var emailAliasDeleteCommand = new Command26("rm").description("Soft-delete an al
2800
3029
  });
2801
3030
 
2802
3031
  // src/generated/cli/email/rm.ts
2803
- import { Command as Command27 } from "commander";
2804
- var emailDeleteCommand = new Command27("rm").description("Soft-delete inbound emails").argument("<id...>", "id").action(async (id, opts) => {
3032
+ import { Command as Command34 } from "commander";
3033
+ var emailDeleteCommand = new Command34("rm").description("Soft-delete inbound emails").argument("<id...>", "id").action(async (id, opts) => {
2805
3034
  const idRaw = id;
2806
3035
  const ids = idRaw.length > 0 ? idRaw : void 0;
2807
3036
  const client2 = await loadSdkClient();
@@ -2823,8 +3052,8 @@ var emailDeleteCommand = new Command27("rm").description("Soft-delete inbound em
2823
3052
  });
2824
3053
 
2825
3054
  // src/generated/cli/domain/rm.ts
2826
- import { Command as Command28 } from "commander";
2827
- var emailDomainDeleteCommand = new Command28("rm").description("Delete a custom email domain").argument("<domain>", "domain").action(async (domain, opts) => {
3055
+ import { Command as Command35 } from "commander";
3056
+ var emailDomainDeleteCommand = new Command35("rm").description("Delete a custom email domain").argument("<domain>", "domain").action(async (domain, opts) => {
2828
3057
  const client2 = await loadSdkClient();
2829
3058
  const result = await emailDomainDelete({
2830
3059
  client: client2._rawClient,
@@ -2844,8 +3073,8 @@ var emailDomainDeleteCommand = new Command28("rm").description("Delete a custom
2844
3073
  });
2845
3074
 
2846
3075
  // src/generated/cli/domain/show.ts
2847
- import { Command as Command29 } from "commander";
2848
- var emailDomainGetCommand = new Command29("show").description("Get one cached custom domain").argument("<domain>", "domain").action(async (domain, opts) => {
3076
+ import { Command as Command36 } from "commander";
3077
+ var emailDomainGetCommand = new Command36("show").description("Get one cached custom domain").argument("<domain>", "domain").action(async (domain, opts) => {
2849
3078
  const client2 = await loadSdkClient();
2850
3079
  const result = await emailDomainGet({
2851
3080
  client: client2._rawClient,
@@ -2865,8 +3094,8 @@ var emailDomainGetCommand = new Command29("show").description("Get one cached cu
2865
3094
  });
2866
3095
 
2867
3096
  // src/generated/cli/email/show.ts
2868
- import { Command as Command30 } from "commander";
2869
- var emailGetCommand = new Command30("show").description("Get an inbound email by id").argument("<id>", "id").option("--include-html <value>", "When `true`, fetch the HTML body from R2 and include it as `html_body` in the response. Costs an extra R2 read; omit if you only need text.").option("--include-deleted <value>", "When `true`, allow fetching a soft-deleted email. Defaults to `false` (returns 404 for soft-deleted rows).").action(async (id, opts) => {
3097
+ import { Command as Command37 } from "commander";
3098
+ var emailGetCommand = new Command37("show").description("Get an inbound email by id").argument("<id>", "id").option("--include-html <value>", "When `true`, fetch the HTML body from R2 and include it as `html_body` in the response. Costs an extra R2 read; omit if you only need text.").option("--include-deleted <value>", "When `true`, allow fetching a soft-deleted email. Defaults to `false` (returns 404 for soft-deleted rows).").action(async (id, opts) => {
2870
3099
  const client2 = await loadSdkClient();
2871
3100
  const result = await emailGet({
2872
3101
  client: client2._rawClient,
@@ -2890,8 +3119,8 @@ var emailGetCommand = new Command30("show").description("Get an inbound email by
2890
3119
  });
2891
3120
 
2892
3121
  // src/generated/cli/email/ls.ts
2893
- import { Command as Command31 } from "commander";
2894
- var emailListCommand = new Command31("ls").description("List inbound emails").option("--limit <value>", "Max items to return (clamped to 1-100). Defaults to 20 server-side.").option("--alias-email <value>", "If set, only return emails received on this full alias email address.").option("--unread-only <value>", "When `true`, only return emails with `is_read=false`.").option("--since <value>", "Unix epoch milliseconds \u2014 only return emails with `received_at >= since`. Useful for incremental sync.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").option("--include-deleted <value>", "When `true`, also return soft-deleted emails. Defaults to `false`.").action(async (opts) => {
3122
+ import { Command as Command38 } from "commander";
3123
+ var emailListCommand = new Command38("ls").description("List inbound emails").option("--limit <value>", "Max items to return (clamped to 1-100). Defaults to 20 server-side.").option("--alias-email <value>", "If set, only return emails received on this full alias email address.").option("--unread-only <value>", "When `true`, only return emails with `is_read=false`.").option("--since <value>", "Unix epoch milliseconds \u2014 only return emails with `received_at >= since`. Useful for incremental sync.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").option("--include-deleted <value>", "When `true`, also return soft-deleted emails. Defaults to `false`.").action(async (opts) => {
2895
3124
  const client2 = await loadSdkClient();
2896
3125
  const result = await emailList({
2897
3126
  client: client2._rawClient,
@@ -2916,8 +3145,8 @@ var emailListCommand = new Command31("ls").description("List inbound emails").op
2916
3145
  });
2917
3146
 
2918
3147
  // src/generated/cli/email/read.ts
2919
- import { Command as Command32 } from "commander";
2920
- var emailMarkReadCommand = new Command32("read").description("Mark inbound emails as read").argument("<id...>", "id").action(async (id, opts) => {
3148
+ import { Command as Command39 } from "commander";
3149
+ var emailMarkReadCommand = new Command39("read").description("Mark inbound emails as read").argument("<id...>", "id").action(async (id, opts) => {
2921
3150
  const idRaw = id;
2922
3151
  const ids = idRaw.length > 0 ? idRaw : void 0;
2923
3152
  const client2 = await loadSdkClient();
@@ -2939,8 +3168,8 @@ var emailMarkReadCommand = new Command32("read").description("Mark inbound email
2939
3168
  });
2940
3169
 
2941
3170
  // src/generated/cli/email/unread.ts
2942
- import { Command as Command33 } from "commander";
2943
- var emailMarkUnreadCommand = new Command33("unread").description("Mark inbound emails as unread").argument("<id...>", "id").action(async (id, opts) => {
3171
+ import { Command as Command40 } from "commander";
3172
+ var emailMarkUnreadCommand = new Command40("unread").description("Mark inbound emails as unread").argument("<id...>", "id").action(async (id, opts) => {
2944
3173
  const idRaw = id;
2945
3174
  const ids = idRaw.length > 0 ? idRaw : void 0;
2946
3175
  const client2 = await loadSdkClient();
@@ -2962,8 +3191,8 @@ var emailMarkUnreadCommand = new Command33("unread").description("Mark inbound e
2962
3191
  });
2963
3192
 
2964
3193
  // src/generated/cli/domain/verify.ts
2965
- import { Command as Command34 } from "commander";
2966
- var emailDomainVerifyCommand = new Command34("verify").description("Verify a custom domain with the provider").argument("<domain>", "domain").action(async (domain, opts) => {
3194
+ import { Command as Command41 } from "commander";
3195
+ var emailDomainVerifyCommand = new Command41("verify").description("Verify a custom domain with the provider").argument("<domain>", "domain").action(async (domain, opts) => {
2967
3196
  const client2 = await loadSdkClient();
2968
3197
  const result = await emailDomainVerify({
2969
3198
  client: client2._rawClient,
@@ -2983,8 +3212,8 @@ var emailDomainVerifyCommand = new Command34("verify").description("Verify a cus
2983
3212
  });
2984
3213
 
2985
3214
  // src/generated/cli/push/config/rm.ts
2986
- import { Command as Command35 } from "commander";
2987
- var pushConfigDeleteCommand = new Command35("rm").description("Remove a push transport").argument("<transport>", "transport").action(async (transport, opts) => {
3215
+ import { Command as Command42 } from "commander";
3216
+ var pushConfigDeleteCommand = new Command42("rm").description("Remove a push transport").argument("<transport>", "transport").action(async (transport, opts) => {
2988
3217
  const client2 = await loadSdkClient();
2989
3218
  const result = await pushConfigDelete({
2990
3219
  client: client2._rawClient,
@@ -3004,8 +3233,8 @@ var pushConfigDeleteCommand = new Command35("rm").description("Remove a push tra
3004
3233
  });
3005
3234
 
3006
3235
  // src/generated/cli/push/config/set.ts
3007
- import { Command as Command36 } from "commander";
3008
- var pushConfigSetCommand = new Command36("set").description("Register or update a push transport").option("--transport <value>", "Transport discriminator. `telegram` is the only supported value today \u2014 push delivers via a Telegram bot DM. Future transports (web push, iOS/Android, generic webhook) will be added as additional discriminator values.").option("--target-bot-username <value>", "Telegram bot username (with leading `@`, 5\u201332 alphanumeric/underscore characters). This is the bot the user has already started a chat with \u2014 wspc DMs notifications to it via the Telegram Bot API.").action(async (opts) => {
3236
+ import { Command as Command43 } from "commander";
3237
+ var pushConfigSetCommand = new Command43("set").description("Register or update a push transport").option("--transport <value>", "Transport discriminator. `telegram` is the only supported value today \u2014 push delivers via a Telegram bot DM. Future transports (web push, iOS/Android, generic webhook) will be added as additional discriminator values.").option("--target-bot-username <value>", "Telegram bot username (with leading `@`, 5\u201332 alphanumeric/underscore characters). This is the bot the user has already started a chat with \u2014 wspc DMs notifications to it via the Telegram Bot API.").action(async (opts) => {
3009
3238
  const client2 = await loadSdkClient();
3010
3239
  const result = await pushConfigSet({
3011
3240
  client: client2._rawClient,
@@ -3028,8 +3257,8 @@ var pushConfigSetCommand = new Command36("set").description("Register or update
3028
3257
  });
3029
3258
 
3030
3259
  // src/generated/cli/push/config/show.ts
3031
- import { Command as Command37 } from "commander";
3032
- var pushConfigGetCommand = new Command37("show").description("List the caller's push transports").action(async (opts) => {
3260
+ import { Command as Command44 } from "commander";
3261
+ var pushConfigGetCommand = new Command44("show").description("List the caller's push transports").action(async (opts) => {
3033
3262
  const client2 = await loadSdkClient();
3034
3263
  const result = await pushConfigGet({
3035
3264
  client: client2._rawClient
@@ -3046,8 +3275,8 @@ var pushConfigGetCommand = new Command37("show").description("List the caller's
3046
3275
  });
3047
3276
 
3048
3277
  // src/generated/cli/push/test.ts
3049
- import { Command as Command38 } from "commander";
3050
- var pushTestCommand = new Command38("test").description("Send a test push notification").option("--transport <value>", "Which transport to send the test message through. Must match a transport the caller has already registered via `POST /push/config`; today only `telegram` is supported.").action(async (opts) => {
3278
+ import { Command as Command45 } from "commander";
3279
+ var pushTestCommand = new Command45("test").description("Send a test push notification").option("--transport <value>", "Which transport to send the test message through. Must match a transport the caller has already registered via `POST /push/config`; today only `telegram` is supported.").action(async (opts) => {
3051
3280
  const client2 = await loadSdkClient();
3052
3281
  const result = await pushTest({
3053
3282
  client: client2._rawClient,
@@ -3070,8 +3299,8 @@ var pushTestCommand = new Command38("test").description("Send a test push notifi
3070
3299
  });
3071
3300
 
3072
3301
  // src/generated/cli/todo/comment/add.ts
3073
- import { Command as Command39 } from "commander";
3074
- var todoCommentCreateCommand = new Command39("add").description("Add a comment to a todo").argument("<id>", "id").argument("<content>", "content").action(async (id, content, opts) => {
3302
+ import { Command as Command46 } from "commander";
3303
+ var todoCommentCreateCommand = new Command46("add").description("Add a comment to a todo").argument("<id>", "id").argument("<content>", "content").action(async (id, content, opts) => {
3075
3304
  const client2 = await loadSdkClient();
3076
3305
  const result = await todoCommentCreate({
3077
3306
  client: client2._rawClient,
@@ -3094,8 +3323,8 @@ var todoCommentCreateCommand = new Command39("add").description("Add a comment t
3094
3323
  });
3095
3324
 
3096
3325
  // src/generated/cli/todo/comment/ls.ts
3097
- import { Command as Command40 } from "commander";
3098
- var todoCommentListCommand = new Command40("ls").description("List comments on a todo").argument("<id>", "id").option("--order <value>", "order").option("--include-deleted <value>", "include_deleted").option("--limit <value>", "Max comments to return. Clamped to [1, 200]. Default 50 server-side.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").action(async (id, opts) => {
3326
+ import { Command as Command47 } from "commander";
3327
+ var todoCommentListCommand = new Command47("ls").description("List comments on a todo").argument("<id>", "id").option("--order <value>", "order").option("--include-deleted <value>", "include_deleted").option("--limit <value>", "Max comments to return. Clamped to [1, 200]. Default 50 server-side.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").action(async (id, opts) => {
3099
3328
  const client2 = await loadSdkClient();
3100
3329
  const result = await todoCommentList({
3101
3330
  client: client2._rawClient,
@@ -3121,8 +3350,8 @@ var todoCommentListCommand = new Command40("ls").description("List comments on a
3121
3350
  });
3122
3351
 
3123
3352
  // src/generated/cli/todo/project/add.ts
3124
- import { Command as Command41 } from "commander";
3125
- var projectCreateCommand = new Command41("add").description("Create a project").argument("<name>", "name").option("--default-todo-type-id <value>", "default_todo_type_id").action(async (name, opts) => {
3353
+ import { Command as Command48 } from "commander";
3354
+ var projectCreateCommand = new Command48("add").description("Create a project").argument("<name>", "name").option("--default-todo-type-id <value>", "default_todo_type_id").action(async (name, opts) => {
3126
3355
  const client2 = await loadSdkClient();
3127
3356
  const result = await projectCreate({
3128
3357
  client: client2._rawClient,
@@ -3143,8 +3372,8 @@ var projectCreateCommand = new Command41("add").description("Create a project").
3143
3372
  });
3144
3373
 
3145
3374
  // src/generated/cli/todo/project/ls.ts
3146
- import { Command as Command42 } from "commander";
3147
- var projectListCommand = new Command42("ls").description("List projects").option("--include-deleted <value>", "Set to `true` to include soft-deleted projects in the response.").action(async (opts) => {
3375
+ import { Command as Command49 } from "commander";
3376
+ var projectListCommand = new Command49("ls").description("List projects").option("--include-deleted <value>", "Set to `true` to include soft-deleted projects in the response.").action(async (opts) => {
3148
3377
  const client2 = await loadSdkClient();
3149
3378
  const result = await projectList({
3150
3379
  client: client2._rawClient,
@@ -3164,8 +3393,8 @@ var projectListCommand = new Command42("ls").description("List projects").option
3164
3393
  });
3165
3394
 
3166
3395
  // src/generated/cli/todo/rule/add.ts
3167
- import { Command as Command43 } from "commander";
3168
- var recurrenceRuleCreateCommand = new Command43("add").description("Create a recurring todo rule").argument("<title>", "title").option("--rrule <value>", "rrule").option("--dtstart <value>", "dtstart").option("--description <value>", "description").option("--parent-id <value>", "parent_id").option("-p, --project <value>", "Project for the recurrence rule, its template todo, and all materialized instances. Must be an active project in the caller's organization.").option("-t, --type <value>", "type_id").action(async (title, opts) => {
3396
+ import { Command as Command50 } from "commander";
3397
+ var recurrenceRuleCreateCommand = new Command50("add").description("Create a recurring todo rule").argument("<title>", "title").option("--rrule <value>", "rrule").option("--dtstart <value>", "dtstart").option("--description <value>", "description").option("--parent-id <value>", "parent_id").option("-p, --project <value>", "Project for the recurrence rule, its template todo, and all materialized instances. Must be an active project in the caller's organization.").option("-t, --type <value>", "type_id").action(async (title, opts) => {
3169
3398
  const client2 = await loadSdkClient();
3170
3399
  const result = await recurrenceRuleCreate({
3171
3400
  client: client2._rawClient,
@@ -3191,8 +3420,8 @@ var recurrenceRuleCreateCommand = new Command43("add").description("Create a rec
3191
3420
  });
3192
3421
 
3193
3422
  // src/generated/cli/todo/rule/ls.ts
3194
- import { Command as Command44 } from "commander";
3195
- var recurrenceRuleListCommand = new Command44("ls").description("List recurring todo rules").option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").action(async (opts) => {
3423
+ import { Command as Command51 } from "commander";
3424
+ var recurrenceRuleListCommand = new Command51("ls").description("List recurring todo rules").option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").action(async (opts) => {
3196
3425
  const client2 = await loadSdkClient();
3197
3426
  const result = await recurrenceRuleList({
3198
3427
  client: client2._rawClient,
@@ -3213,7 +3442,7 @@ var recurrenceRuleListCommand = new Command44("ls").description("List recurring
3213
3442
  });
3214
3443
 
3215
3444
  // src/generated/cli/todo/add.ts
3216
- import { Command as Command45 } from "commander";
3445
+ import { Command as Command52 } from "commander";
3217
3446
 
3218
3447
  // src/handwritten/utils/parse-json-field.ts
3219
3448
  function parseJsonField(raw, flag) {
@@ -3228,7 +3457,7 @@ function parseJsonField(raw, flag) {
3228
3457
  }
3229
3458
 
3230
3459
  // src/generated/cli/todo/add.ts
3231
- var todoCreateCommand = new Command45("add").description("Create a todo").argument("<title>", "title").option("-p, --project <value>", "Project id to assign this todo to. It must be an active project in the caller's organization.").option("--description <value>", "Free-form details about the todo. Fully supports GFM Markdown (tables, strikethrough, task lists). Stored verbatim; client applications are responsible for rendering. Optional. Passing `null` is strictly rejected.").option("--parent-id <value>", "Parent todo ID (`tod_<ULID>`) to attach this todo as a child under another todo. Omit or pass `null` to create a root-level todo. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`. To make a subtask appear on every occurrence of a recurring rule, set this to that rule's template todo id (the template id returned when the rule is created); the server re-materializes future occurrences so each carries the subtask.").option("--status <value>", "Initial status of the todo. Omit to default to `open`. Allowed values: `open`, `in_progress`, `done`, `cancelled`.").option("--due-at <value>", 'Optional calendar due date in ISO date-only format (`YYYY-MM-DD`). Stored without timezone offsets to represent the same local calendar day globally. Pass `""` or omit the field to skip setting a due date. Passing `null` is strictly rejected.').option("--idempotency-key <value>", "idempotency_key").option("--type-id <value>", "Type id this todo belongs to. Omit to use the project's default type. When project_id is also supplied, the type must belong to the same project. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "Custom field values keyed by the field's immutable `key` (not the human `label`). Each value must match the declared field type: string fields require string values, and string_array fields require string arrays. Providing a key that is not declared on the resolved todo type is strictly rejected with `UNDECLARED_FIELD`. Missing required fields that lack a default value are rejected with `FIELD_REQUIRED`. Defaults declared on the type are auto-applied at create time.").action(async (title, opts) => {
3460
+ var todoCreateCommand = new Command52("add").description("Create a todo").argument("<title>", "title").option("-p, --project <value>", "Project id to assign this todo to. It must be an active project in the caller's organization.").option("--description <value>", "Free-form details about the todo. Fully supports GFM Markdown (tables, strikethrough, task lists). Stored verbatim; client applications are responsible for rendering. Optional. Passing `null` is strictly rejected.").option("--parent-id <value>", "Parent todo ID (`tod_<ULID>`) to attach this todo as a child under another todo. Omit or pass `null` to create a root-level todo. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`. To make a subtask appear on every occurrence of a recurring rule, set this to that rule's template todo id (the template id returned when the rule is created); the server re-materializes future occurrences so each carries the subtask.").option("--status <value>", "Initial status of the todo. Omit to default to `open`. Allowed values: `open`, `in_progress`, `done`, `cancelled`.").option("--due-at <value>", 'Optional calendar due date in ISO date-only format (`YYYY-MM-DD`). Stored without timezone offsets to represent the same local calendar day globally. Pass `""` or omit the field to skip setting a due date. Passing `null` is strictly rejected.').option("--idempotency-key <value>", "idempotency_key").option("--type-id <value>", "Type id this todo belongs to. Omit to use the project's default type. When project_id is also supplied, the type must belong to the same project. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "Custom field values keyed by the field's immutable `key` (not the human `label`). Each value must match the declared field type: string fields require string values, and string_array fields require string arrays. Providing a key that is not declared on the resolved todo type is strictly rejected with `UNDECLARED_FIELD`. Missing required fields that lack a default value are rejected with `FIELD_REQUIRED`. Defaults declared on the type are auto-applied at create time.").action(async (title, opts) => {
3232
3461
  const client2 = await loadSdkClient();
3233
3462
  const result = await todoCreate({
3234
3463
  client: client2._rawClient,
@@ -3256,8 +3485,8 @@ var todoCreateCommand = new Command45("add").description("Create a todo").argume
3256
3485
  });
3257
3486
 
3258
3487
  // src/generated/cli/todo/ls.ts
3259
- import { Command as Command46 } from "commander";
3260
- var todoListCommand = new Command46("ls").description("List todos with filters").option("-p, --project <value>", "Filter by project. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").option("--parent-id <value>", "parent_id").option("-s, --status <value>", "status").option("--include-deleted <value>", "include_deleted").option("--include-templates <value>", "include_templates").option("--due-after <value>", "due_after").option("--due-before <value>", "due_before").option("--type-id <value>", "type_id").option("--sort-by <value>", "sort_by").option("--order <value>", "order").option("--include-orphan-fields <value>", "include_orphan_fields").option("--limit <value>", "Max todos to return. Clamped to [1, 200]. Default 50 server-side.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").action(async (opts) => {
3488
+ import { Command as Command53 } from "commander";
3489
+ var todoListCommand = new Command53("ls").description("List todos with filters").option("-p, --project <value>", "Filter by project. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").option("--parent-id <value>", "parent_id").option("-s, --status <value>", "status").option("--include-deleted <value>", "include_deleted").option("--include-templates <value>", "include_templates").option("--due-after <value>", "due_after").option("--due-before <value>", "due_before").option("--type-id <value>", "type_id").option("--sort-by <value>", "sort_by").option("--order <value>", "order").option("--include-orphan-fields <value>", "include_orphan_fields").option("--limit <value>", "Max todos to return. Clamped to [1, 200]. Default 50 server-side.").option("--cursor <value>", "Opaque pagination cursor returned in `next_cursor` of a previous response.").action(async (opts) => {
3261
3490
  const client2 = await loadSdkClient();
3262
3491
  const result = await todoList({
3263
3492
  client: client2._rawClient,
@@ -3290,8 +3519,8 @@ var todoListCommand = new Command46("ls").description("List todos with filters")
3290
3519
  });
3291
3520
 
3292
3521
  // src/generated/cli/todo/type/ls.ts
3293
- import { Command as Command47 } from "commander";
3294
- var todoTypeListCommand = new Command47("ls").description("List todo types").option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").option("--include-deleted <value>", "include_deleted").action(async (opts) => {
3522
+ import { Command as Command54 } from "commander";
3523
+ var todoTypeListCommand = new Command54("ls").description("List todo types").option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.").option("--user-id <value>", "user_id").option("--include-deleted <value>", "include_deleted").action(async (opts) => {
3295
3524
  const client2 = await loadSdkClient();
3296
3525
  const result = await todoTypeList({
3297
3526
  client: client2._rawClient,
@@ -3313,8 +3542,8 @@ var todoTypeListCommand = new Command47("ls").description("List todo types").opt
3313
3542
  });
3314
3543
 
3315
3544
  // src/generated/cli/todo/comment/rm.ts
3316
- import { Command as Command48 } from "commander";
3317
- var todoCommentDeleteCommand = new Command48("rm").description("Soft-delete a comment").argument("<id>", "id").action(async (id, opts) => {
3545
+ import { Command as Command55 } from "commander";
3546
+ var todoCommentDeleteCommand = new Command55("rm").description("Soft-delete a comment").argument("<id>", "id").action(async (id, opts) => {
3318
3547
  const client2 = await loadSdkClient();
3319
3548
  const result = await todoCommentDelete({
3320
3549
  client: client2._rawClient,
@@ -3334,8 +3563,8 @@ var todoCommentDeleteCommand = new Command48("rm").description("Soft-delete a co
3334
3563
  });
3335
3564
 
3336
3565
  // src/generated/cli/todo/comment/edit.ts
3337
- import { Command as Command49 } from "commander";
3338
- var todoCommentUpdateCommand = new Command49("edit").description("Edit a comment").argument("<id>", "id").argument("<content>", "content").action(async (id, content, opts) => {
3566
+ import { Command as Command56 } from "commander";
3567
+ var todoCommentUpdateCommand = new Command56("edit").description("Edit a comment").argument("<id>", "id").argument("<content>", "content").action(async (id, content, opts) => {
3339
3568
  const client2 = await loadSdkClient();
3340
3569
  const result = await todoCommentUpdate({
3341
3570
  client: client2._rawClient,
@@ -3358,8 +3587,8 @@ var todoCommentUpdateCommand = new Command49("edit").description("Edit a comment
3358
3587
  });
3359
3588
 
3360
3589
  // src/generated/cli/todo/rule/rm.ts
3361
- import { Command as Command50 } from "commander";
3362
- var recurrenceRuleDeleteCommand = new Command50("rm").description("Delete a recurring todo rule").argument("<id>", "id").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
3590
+ import { Command as Command57 } from "commander";
3591
+ var recurrenceRuleDeleteCommand = new Command57("rm").description("Delete a recurring todo rule").argument("<id>", "id").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
3363
3592
  const client2 = await loadSdkClient();
3364
3593
  const result = await recurrenceRuleDelete({
3365
3594
  client: client2._rawClient,
@@ -3382,8 +3611,8 @@ var recurrenceRuleDeleteCommand = new Command50("rm").description("Delete a recu
3382
3611
  });
3383
3612
 
3384
3613
  // src/generated/cli/todo/rule/show.ts
3385
- import { Command as Command51 } from "commander";
3386
- var recurrenceRuleGetCommand = new Command51("show").description("Get a recurring todo rule").argument("<id>", "id").action(async (id, opts) => {
3614
+ import { Command as Command58 } from "commander";
3615
+ var recurrenceRuleGetCommand = new Command58("show").description("Get a recurring todo rule").argument("<id>", "id").action(async (id, opts) => {
3387
3616
  const client2 = await loadSdkClient();
3388
3617
  const result = await recurrenceRuleGet({
3389
3618
  client: client2._rawClient,
@@ -3403,8 +3632,8 @@ var recurrenceRuleGetCommand = new Command51("show").description("Get a recurrin
3403
3632
  });
3404
3633
 
3405
3634
  // src/generated/cli/todo/rm.ts
3406
- import { Command as Command52 } from "commander";
3407
- var todoDeleteCommand = new Command52("rm").description("Soft-delete a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--cascade <value>", "cascade").action(async (id, opts) => {
3635
+ import { Command as Command59 } from "commander";
3636
+ var todoDeleteCommand = new Command59("rm").description("Soft-delete a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--cascade <value>", "cascade").action(async (id, opts) => {
3408
3637
  const client2 = await loadSdkClient();
3409
3638
  const result = await todoDelete({
3410
3639
  client: client2._rawClient,
@@ -3428,8 +3657,8 @@ var todoDeleteCommand = new Command52("rm").description("Soft-delete a todo").ar
3428
3657
  });
3429
3658
 
3430
3659
  // src/generated/cli/todo/show.ts
3431
- import { Command as Command53 } from "commander";
3432
- var todoGetCommand = new Command53("show").description("Get a todo by id").argument("<id>", "id").option("--include-deleted <value>", "include_deleted").option("--include-orphan-fields <value>", "include_orphan_fields").action(async (id, opts) => {
3660
+ import { Command as Command60 } from "commander";
3661
+ var todoGetCommand = new Command60("show").description("Get a todo by id").argument("<id>", "id").option("--include-deleted <value>", "include_deleted").option("--include-orphan-fields <value>", "include_orphan_fields").action(async (id, opts) => {
3433
3662
  const client2 = await loadSdkClient();
3434
3663
  const result = await todoGet({
3435
3664
  client: client2._rawClient,
@@ -3454,8 +3683,8 @@ var todoGetCommand = new Command53("show").description("Get a todo by id").argum
3454
3683
  });
3455
3684
 
3456
3685
  // src/generated/cli/todo/update.ts
3457
- import { Command as Command54 } from "commander";
3458
- var todoUpdateCommand = new Command54("update").description("Update a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--title <value>", "New title. Omit to leave the existing title unchanged. Must be non-empty when supplied.").option("--description <value>", 'New description. Markdown formatted (CommonMark + GFM tables, strikethrough, task lists). Pass empty string `""` explicitly to clear an existing description, or omit to leave unchanged. Passing `null` is strictly rejected.').option("--parent-id <value>", "Re-parent the todo. Pass a valid parent ID to attach under another todo, pass `null` to move it back to the root level, or omit to leave unchanged. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`.").option("--status <value>", "New status of the todo. Allowed transitions: `open` \u2794 `in_progress` \u2794 `done`. `cancelled` represents a terminal state. Transitioning to `done` automatically emits a `captureTodoCompleted` analytics event. Omit to leave the existing status unchanged.").option("--due-at <value>", 'Update calendar due date in ISO date-only format (`YYYY-MM-DD`). Pass `""` explicitly to clear an existing due date, or omit to leave it unchanged. Passing `null` is strictly rejected.').option("--type-id <value>", "Re-assign this todo to a different active type. The new type must belong to the todo's same project; otherwise the request fails with TYPE_PROJECT_MISMATCH. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "PATCH semantics: only the keys present in this map change. Pass `null` for a key (e.g. `custom_fields: { priority: null }`) to explicitly delete that custom field value. Array values are replaced wholesale with no element-level diff. Providing a key that is not declared on the effective todo type is rejected with `UNDECLARED_FIELD`.").option("--user-id <value>", "Reassign the owner (assignee) user ID of this todo. Target user must belong to the same organization.").action(async (id, opts) => {
3686
+ import { Command as Command61 } from "commander";
3687
+ var todoUpdateCommand = new Command61("update").description("Update a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--title <value>", "New title. Omit to leave the existing title unchanged. Must be non-empty when supplied.").option("--description <value>", 'New description. Markdown formatted (CommonMark + GFM tables, strikethrough, task lists). Pass empty string `""` explicitly to clear an existing description, or omit to leave unchanged. Passing `null` is strictly rejected.').option("--parent-id <value>", "Re-parent the todo. Pass a valid parent ID to attach under another todo, pass `null` to move it back to the root level, or omit to leave unchanged. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`.").option("--status <value>", "New status of the todo. Allowed transitions: `open` \u2794 `in_progress` \u2794 `done`. `cancelled` represents a terminal state. Transitioning to `done` automatically emits a `captureTodoCompleted` analytics event. Omit to leave the existing status unchanged.").option("--due-at <value>", 'Update calendar due date in ISO date-only format (`YYYY-MM-DD`). Pass `""` explicitly to clear an existing due date, or omit to leave it unchanged. Passing `null` is strictly rejected.').option("--type-id <value>", "Re-assign this todo to a different active type. The new type must belong to the todo's same project; otherwise the request fails with TYPE_PROJECT_MISMATCH. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "PATCH semantics: only the keys present in this map change. Pass `null` for a key (e.g. `custom_fields: { priority: null }`) to explicitly delete that custom field value. Array values are replaced wholesale with no element-level diff. Providing a key that is not declared on the effective todo type is rejected with `UNDECLARED_FIELD`.").option("--user-id <value>", "Reassign the owner (assignee) user ID of this todo. Target user must belong to the same organization.").action(async (id, opts) => {
3459
3688
  const client2 = await loadSdkClient();
3460
3689
  const result = await todoUpdate({
3461
3690
  client: client2._rawClient,
@@ -3514,6 +3743,17 @@ function registerGeneratedCommands(root) {
3514
3743
  root_event.addCommand(eventGetCommand);
3515
3744
  root_event.addCommand(eventUpdateCommand);
3516
3745
  root_event.addCommand(eventIcsDownloadCommand);
3746
+ const root_drive = root.command("drive").description("drive commands");
3747
+ const root_drive_library = root_drive.command("library").description("library commands");
3748
+ root_drive_library.addCommand(driveLibraryCreateCommand);
3749
+ root_drive_library.addCommand(driveLibraryListCommand);
3750
+ root_drive_library.addCommand(driveLibraryDeleteCommand);
3751
+ root_drive_library.addCommand(driveLibraryGetCommand);
3752
+ root_drive_library.addCommand(driveLibraryUpdateCommand);
3753
+ const root_drive_file = root_drive.command("file").description("file commands");
3754
+ root_drive_file.addCommand(driveFileDeleteCommand);
3755
+ const root_drive_manifest = root_drive.command("manifest").description("manifest commands");
3756
+ root_drive_manifest.addCommand(driveManifestGetCommand);
3517
3757
  const root_alias = root.command("alias").description("alias commands");
3518
3758
  root_alias.addCommand(emailAliasCreateCommand);
3519
3759
  root_alias.addCommand(emailAliasListCommand);
@@ -3560,7 +3800,7 @@ function registerGeneratedCommands(root) {
3560
3800
  }
3561
3801
 
3562
3802
  // src/handwritten/commands/login.ts
3563
- import { Command as Command55 } from "commander";
3803
+ import { Command as Command62 } from "commander";
3564
3804
 
3565
3805
  // src/handwritten/auth/device-flow.ts
3566
3806
  var DEFAULT_SLEEP = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3794,7 +4034,7 @@ function resolveLoginTarget(opts, env) {
3794
4034
  function wantsJson(opts, env) {
3795
4035
  return opts.json === true || env.WSPC_OUTPUT === "json";
3796
4036
  }
3797
- var loginCommand = new Command55("login").description("Log in via OAuth device flow (default) or API key").option("--api-key <key>", "Log in with a wspc API key (escape hatch)").option("--api-base <url>", "Target API base URL (default: production)").option("--env <name>", "Config env name to store credentials under").option("--json", "Emit machine-readable events to stdout").action(async (opts) => {
4037
+ var loginCommand = new Command62("login").description("Log in via OAuth device flow (default) or API key").option("--api-key <key>", "Log in with a wspc API key (escape hatch)").option("--api-base <url>", "Target API base URL (default: production)").option("--env <name>", "Config env name to store credentials under").option("--json", "Emit machine-readable events to stdout").action(async (opts) => {
3798
4038
  const store = new ConfigStore();
3799
4039
  const { baseUrl, envName } = resolveLoginTarget(opts, process.env);
3800
4040
  const output = wantsJson(opts, process.env) ? { write: () => {
@@ -3813,7 +4053,7 @@ var loginCommand = new Command55("login").description("Log in via OAuth device f
3813
4053
  });
3814
4054
 
3815
4055
  // src/handwritten/commands/logout.ts
3816
- import { Command as Command56 } from "commander";
4056
+ import { Command as Command63 } from "commander";
3817
4057
 
3818
4058
  // src/handwritten/auth/logout.ts
3819
4059
  async function runLogout(opts) {
@@ -3841,7 +4081,7 @@ async function runLogout(opts) {
3841
4081
  }
3842
4082
 
3843
4083
  // src/handwritten/commands/logout.ts
3844
- var logoutCommand = new Command56("logout").description("Log out an account (default: the active account in the current env)").argument("[email]", "Email of the account to log out").option("--all", "Log out every account in the current env").action(async (email, opts) => {
4084
+ var logoutCommand = new Command63("logout").description("Log out an account (default: the active account in the current env)").argument("[email]", "Email of the account to log out").option("--all", "Log out every account in the current env").action(async (email, opts) => {
3845
4085
  const res = await runLogout({ store: new ConfigStore(), email, all: opts.all });
3846
4086
  if (res.removed.length === 0) {
3847
4087
  process.stdout.write("nothing to log out\n");
@@ -3854,7 +4094,7 @@ var logoutCommand = new Command56("logout").description("Log out an account (def
3854
4094
  });
3855
4095
 
3856
4096
  // src/handwritten/commands/whoami.ts
3857
- import { Command as Command57 } from "commander";
4097
+ import { Command as Command64 } from "commander";
3858
4098
  var ENV_DISPLAY = {
3859
4099
  shape: "object",
3860
4100
  fields: ["name", "api_base", "account", "actor", "agent_label"]
@@ -3886,7 +4126,7 @@ async function backfillActiveEmail(store, envName, email, userId) {
3886
4126
  await store.write(cfg);
3887
4127
  }
3888
4128
  }
3889
- var whoamiCommand = new Command57("whoami").description("Show the active env, signed-in account, and organization").action(async () => {
4129
+ var whoamiCommand = new Command64("whoami").description("Show the active env, signed-in account, and organization").action(async () => {
3890
4130
  const store = new ConfigStore();
3891
4131
  const config = await store.read();
3892
4132
  let resolved;
@@ -3939,8 +4179,8 @@ function printLoggedOut() {
3939
4179
  }
3940
4180
 
3941
4181
  // src/handwritten/commands/config.ts
3942
- import { Command as Command58 } from "commander";
3943
- var configCommand = new Command58("config").description("Manage wspc local config");
4182
+ import { Command as Command65 } from "commander";
4183
+ var configCommand = new Command65("config").description("Manage wspc local config");
3944
4184
  registerRenderer("config_show", (data) => {
3945
4185
  const d = data;
3946
4186
  if (d.envs.length === 0) {
@@ -4011,7 +4251,7 @@ configCommand.command("use <env>").description("Switch current_env").action(asyn
4011
4251
  });
4012
4252
 
4013
4253
  // src/handwritten/commands/account.ts
4014
- import { Command as Command59 } from "commander";
4254
+ import { Command as Command66 } from "commander";
4015
4255
  async function listAccounts(store) {
4016
4256
  const c = await store.read();
4017
4257
  const envName = c.current_env;
@@ -4052,7 +4292,7 @@ registerRenderer("account_ls", (data) => {
4052
4292
  ]);
4053
4293
  process.stdout.write(table(headers, body));
4054
4294
  });
4055
- var accountCommand = new Command59("account").description("Manage logged-in accounts");
4295
+ var accountCommand = new Command66("account").description("Manage logged-in accounts");
4056
4296
  accountCommand.command("ls").description("List accounts in the current env (active marked with \u2713)").action(async () => {
4057
4297
  const accounts = await listAccounts(new ConfigStore());
4058
4298
  render({ kind: "account_ls" }, { accounts });
@@ -4064,7 +4304,7 @@ accountCommand.command("switch <email>").description("Set the active account for
4064
4304
  });
4065
4305
 
4066
4306
  // src/handwritten/commands/todo-done.ts
4067
- import { Command as Command60 } from "commander";
4307
+ import { Command as Command67 } from "commander";
4068
4308
  var TODO_UPDATE_DISPLAY = {
4069
4309
  shape: "object",
4070
4310
  format: {
@@ -4082,7 +4322,7 @@ var TODO_UPDATE_DISPLAY = {
4082
4322
  deleted_at: "relative-time"
4083
4323
  }
4084
4324
  };
4085
- var todoDoneCommand = new Command60("done").description("Mark a todo done (sugar for `update <id> --status done`)").argument("<id>", "Todo id").action(async (id) => {
4325
+ var todoDoneCommand = new Command67("done").description("Mark a todo done (sugar for `update <id> --status done`)").argument("<id>", "Todo id").action(async (id) => {
4086
4326
  const client2 = await loadSdkClient();
4087
4327
  const result = await todoUpdate({
4088
4328
  client: client2._rawClient,
@@ -4101,7 +4341,7 @@ var todoDoneCommand = new Command60("done").description("Mark a todo done (sugar
4101
4341
  });
4102
4342
 
4103
4343
  // src/handwritten/commands/email/send.ts
4104
- import { Command as Command61 } from "commander";
4344
+ import { Command as Command68 } from "commander";
4105
4345
  import { readFile, stat } from "fs/promises";
4106
4346
  import { basename } from "path";
4107
4347
 
@@ -4159,7 +4399,7 @@ async function resolveAttachment(input) {
4159
4399
  `--attach ${input}: neither a readable file nor a valid <prefix>_<ulid>:<idx> reference.`
4160
4400
  );
4161
4401
  }
4162
- var sendCommand = new Command61("send").description("Send an outbound email").requiredOption("--from <alias-email>", "alias email to send from").option("--to <addr...>", "recipient address (repeatable)", []).option("--subject <text>", "subject").option("--text <body>", "plain-text body").option("--text-file <path>", "read text body from file").option("--reply <id>", "inbound email id to reply to").option("--attach <path-or-ref...>", "attachment (file path or eml_xxx:idx)", []).requiredOption("--idempotency-key <key>", "idempotency key").action(async (opts) => {
4402
+ var sendCommand = new Command68("send").description("Send an outbound email").requiredOption("--from <alias-email>", "alias email to send from").option("--to <addr...>", "recipient address (repeatable)", []).option("--subject <text>", "subject").option("--text <body>", "plain-text body").option("--text-file <path>", "read text body from file").option("--reply <id>", "inbound email id to reply to").option("--attach <path-or-ref...>", "attachment (file path or eml_xxx:idx)", []).requiredOption("--idempotency-key <key>", "idempotency key").action(async (opts) => {
4163
4403
  const isReply = Boolean(opts.reply);
4164
4404
  const to = opts.to;
4165
4405
  const attachInputs = opts.attach;
@@ -4246,7 +4486,7 @@ var sendCommand = new Command61("send").description("Send an outbound email").re
4246
4486
  });
4247
4487
 
4248
4488
  // src/handwritten/commands/email/attachment.ts
4249
- import { Command as Command62 } from "commander";
4489
+ import { Command as Command69 } from "commander";
4250
4490
  import { createWriteStream } from "fs";
4251
4491
  import { Readable } from "stream";
4252
4492
  import { pipeline } from "stream/promises";
@@ -4263,7 +4503,7 @@ function parseContentDispositionFilename(header) {
4263
4503
  }
4264
4504
 
4265
4505
  // src/handwritten/commands/email/attachment.ts
4266
- var attachmentCommand = new Command62("attachment").description("Download an inbound email attachment by index").argument("<email-id>").argument("<idx>").option("--output <path>", "output file path").option("--include-deleted", "allow downloads from soft-deleted parent emails").action(async (emailId, idxArg, opts) => {
4506
+ var attachmentCommand = new Command69("attachment").description("Download an inbound email attachment by index").argument("<email-id>").argument("<idx>").option("--output <path>", "output file path").option("--include-deleted", "allow downloads from soft-deleted parent emails").action(async (emailId, idxArg, opts) => {
4267
4507
  const idx = Number(idxArg);
4268
4508
  if (!Number.isInteger(idx) || idx < 0) {
4269
4509
  process.stderr.write(`<idx> must be a non-negative integer (got "${idxArg}")
@@ -4295,7 +4535,7 @@ var attachmentCommand = new Command62("attachment").description("Download an inb
4295
4535
  });
4296
4536
 
4297
4537
  // src/handwritten/commands/drive/bind.ts
4298
- import { Command as Command63 } from "commander";
4538
+ import { Command as Command70 } from "commander";
4299
4539
  import { stat as stat2 } from "fs/promises";
4300
4540
  import { resolve } from "path";
4301
4541
 
@@ -4367,9 +4607,12 @@ async function createDriveApi(opts = {}) {
4367
4607
  }
4368
4608
  return payload;
4369
4609
  },
4370
- async downloadFile(id, path) {
4610
+ async downloadFile(id, path, versionId) {
4371
4611
  const url = driveContentUrl(client2.baseUrl, id);
4372
4612
  url.searchParams.set("path", path);
4613
+ if (versionId !== void 0) {
4614
+ url.searchParams.set("version_id", versionId);
4615
+ }
4373
4616
  const res = await client2.fetch(url, { method: "GET" });
4374
4617
  if (!res.ok) {
4375
4618
  const text = await res.text();
@@ -4384,11 +4627,34 @@ async function createDriveApi(opts = {}) {
4384
4627
  import { mkdir, open, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
4385
4628
  import { randomUUID } from "crypto";
4386
4629
  import { join as join2 } from "path";
4387
- var DRIVE_DIR = ".wspc-drive";
4388
- var STATE_FILE = "state.json";
4389
- function statePath(root) {
4390
- return join2(root, DRIVE_DIR, STATE_FILE);
4391
- }
4630
+ import { DateTime as DateTime4 } from "luxon";
4631
+
4632
+ // src/handwritten/commands/drive/clock.ts
4633
+ import { DateTime as DateTime3 } from "luxon";
4634
+ var systemDriveClock = {
4635
+ now: () => DateTime3.utc()
4636
+ };
4637
+ function driveIsoTimestamp(clock = systemDriveClock) {
4638
+ const timestamp = clock.now().toISO();
4639
+ if (timestamp === null) {
4640
+ throw new Error("invalid drive clock timestamp");
4641
+ }
4642
+ return timestamp;
4643
+ }
4644
+ function driveConflictTimestamp(clock = systemDriveClock) {
4645
+ const timestamp = clock.now().toUTC();
4646
+ if (!timestamp.isValid) {
4647
+ throw new Error("invalid drive clock timestamp");
4648
+ }
4649
+ return timestamp.toFormat("yyyyLLdd'T'HHmmss'Z'");
4650
+ }
4651
+
4652
+ // src/handwritten/commands/drive/state.ts
4653
+ var DRIVE_DIR = ".wspc-drive";
4654
+ var STATE_FILE = "state.json";
4655
+ function statePath(root) {
4656
+ return join2(root, DRIVE_DIR, STATE_FILE);
4657
+ }
4392
4658
  async function readDriveState(root) {
4393
4659
  const buf = await readFile2(statePath(root), "utf8");
4394
4660
  const parsed = JSON.parse(buf);
@@ -4397,13 +4663,13 @@ async function readDriveState(root) {
4397
4663
  }
4398
4664
  return parsed;
4399
4665
  }
4400
- async function writeDriveState(root, state) {
4666
+ async function writeDriveState(root, state, clock) {
4401
4667
  await mkdir(join2(root, DRIVE_DIR), { recursive: true });
4402
4668
  const tmp = join2(root, DRIVE_DIR, `state.json.tmp-${process.pid}-${randomUUID()}`);
4403
4669
  const snapshot = JSON.stringify(
4404
4670
  {
4405
4671
  ...state,
4406
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
4672
+ updated_at: driveIsoTimestamp(clock)
4407
4673
  },
4408
4674
  null,
4409
4675
  2
@@ -4422,7 +4688,7 @@ async function writeDriveState(root, state) {
4422
4688
  await rm(tmp, { force: true });
4423
4689
  }
4424
4690
  }
4425
- async function initDriveState(root, libraryId) {
4691
+ async function initDriveState(root, libraryId, clock) {
4426
4692
  await mkdir(join2(root, DRIVE_DIR), { recursive: true });
4427
4693
  try {
4428
4694
  const existing = await readDriveState(root);
@@ -4433,7 +4699,7 @@ async function initDriveState(root, libraryId) {
4433
4699
  } catch (err) {
4434
4700
  if (err.code !== "ENOENT") throw err;
4435
4701
  }
4436
- const now = (/* @__PURE__ */ new Date()).toISOString();
4702
+ const now = driveIsoTimestamp(clock);
4437
4703
  const state = {
4438
4704
  schema_version: 1,
4439
4705
  library_id: libraryId,
@@ -4442,9 +4708,32 @@ async function initDriveState(root, libraryId) {
4442
4708
  entries: {},
4443
4709
  conflicts: {}
4444
4710
  };
4445
- await writeDriveState(root, state);
4711
+ await writeDriveState(root, state, clock);
4446
4712
  return state;
4447
4713
  }
4714
+ async function ensureDriveRealtimeState(root) {
4715
+ return withDriveLock(root, async () => {
4716
+ const state = await readDriveState(root);
4717
+ if (state.realtime?.client_id !== void 0) {
4718
+ return state;
4719
+ }
4720
+ const next = {
4721
+ ...state,
4722
+ realtime: {
4723
+ ...state.realtime,
4724
+ client_id: `drvcli_${randomUUID().replace(/-/g, "")}`
4725
+ }
4726
+ };
4727
+ await writeDriveState(root, next);
4728
+ return next;
4729
+ });
4730
+ }
4731
+ async function writeDriveRealtimeState(root, realtime) {
4732
+ await withDriveLock(root, async () => {
4733
+ const current = await readDriveState(root);
4734
+ await writeDriveState(root, { ...current, realtime });
4735
+ });
4736
+ }
4448
4737
  async function withDriveLock(root, fn) {
4449
4738
  await mkdir(join2(root, DRIVE_DIR), { recursive: true });
4450
4739
  const lockFile = join2(root, DRIVE_DIR, "sync.lock");
@@ -4463,18 +4752,28 @@ async function withDriveLock(root, fn) {
4463
4752
  });
4464
4753
  }
4465
4754
  }
4466
- function isRecord(value) {
4755
+ function isRecord2(value) {
4467
4756
  return typeof value === "object" && value !== null && !Array.isArray(value);
4468
4757
  }
4469
4758
  function isDriveStateEntry(value) {
4470
- return isRecord(value) && typeof value.entry_id === "string" && typeof value.entry_version === "number" && typeof value.size_bytes === "number" && typeof value.last_synced_at === "string" && value.status === "synced" && (value.current_version_id === void 0 || typeof value.current_version_id === "string") && (value.content_sha256 === void 0 || typeof value.content_sha256 === "string") && (value.last_local_sha256 === void 0 || typeof value.last_local_sha256 === "string");
4759
+ return isRecord2(value) && typeof value.entry_id === "string" && typeof value.entry_version === "number" && typeof value.size_bytes === "number" && typeof value.last_synced_at === "string" && value.status === "synced" && (value.current_version_id === void 0 || typeof value.current_version_id === "string") && (value.content_sha256 === void 0 || typeof value.content_sha256 === "string") && (value.last_local_sha256 === void 0 || typeof value.last_local_sha256 === "string");
4471
4760
  }
4472
4761
  function isDriveConflict(value) {
4473
- return isRecord(value) && typeof value.detected_at === "string" && typeof value.reason === "string" && (value.remote_entry_version === void 0 || typeof value.remote_entry_version === "number") && (value.remote_version_id === void 0 || typeof value.remote_version_id === "string");
4762
+ return isRecord2(value) && typeof value.detected_at === "string" && typeof value.reason === "string" && (value.type === void 0 || value.type === "edit_edit" || value.type === "create_create" || value.type === "delete_edit" || value.type === "edit_delete") && (value.strategy === void 0 || value.strategy === "clean_merge" || value.strategy === "conflict_copy" || value.strategy === "record_only") && (value.base_version_id === void 0 || typeof value.base_version_id === "string") && (value.remote_entry_version === void 0 || typeof value.remote_entry_version === "number") && (value.remote_version_id === void 0 || typeof value.remote_version_id === "string") && (value.conflict_paths === void 0 || Array.isArray(value.conflict_paths) && value.conflict_paths.every((path) => typeof path === "string"));
4763
+ }
4764
+ function isDriveRealtimeState(value) {
4765
+ const allowedKeys = /* @__PURE__ */ new Set(["client_id", "last_cursor", "last_connected_at", "last_event_at"]);
4766
+ return isRecord2(value) && Object.keys(value).every((key) => allowedKeys.has(key)) && (value.client_id === void 0 || typeof value.client_id === "string" && /^drvcli_[A-Za-z0-9_-]+$/.test(value.client_id)) && (value.last_cursor === void 0 || typeof value.last_cursor === "string") && isOptionalIsoTimestamp(value.last_connected_at) && isOptionalIsoTimestamp(value.last_event_at);
4767
+ }
4768
+ function isOptionalIsoTimestamp(value) {
4769
+ if (value === void 0) return true;
4770
+ if (typeof value !== "string") return false;
4771
+ if (!/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value)) return false;
4772
+ return DateTime4.fromISO(value, { setZone: true }).isValid;
4474
4773
  }
4475
4774
  function isValidDriveState(value) {
4476
- if (!isRecord(value)) return false;
4477
- if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !isRecord(value.entries) || !isRecord(value.conflicts)) {
4775
+ if (!isRecord2(value)) return false;
4776
+ if (value.schema_version !== 1 || typeof value.library_id !== "string" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || !isRecord2(value.entries) || !isRecord2(value.conflicts) || value.realtime !== void 0 && !isDriveRealtimeState(value.realtime)) {
4478
4777
  return false;
4479
4778
  }
4480
4779
  for (const entry of Object.values(value.entries)) {
@@ -4499,7 +4798,7 @@ async function assertExistingDirectory(path) {
4499
4798
  }
4500
4799
  }
4501
4800
  function driveBindCommand() {
4502
- return new Command63("bind").description("Bind a local folder to an existing Drive library").requiredOption("--library <id>", "existing Drive library id").argument("[path]", "local folder path", ".").action(async (path, opts) => {
4801
+ return new Command70("bind").description("Bind a local folder to an existing Drive library").requiredOption("--library <id>", "existing Drive library id").argument("[path]", "local folder path", ".").action(async (path, opts) => {
4503
4802
  const root = resolve(path);
4504
4803
  await assertExistingDirectory(root);
4505
4804
  const api = await createDriveApi();
@@ -4517,13 +4816,10 @@ function driveBindCommand() {
4517
4816
  }
4518
4817
 
4519
4818
  // src/handwritten/commands/drive/sync.ts
4520
- import { Command as Command64 } from "commander";
4521
- import { createWriteStream as createWriteStream2 } from "fs";
4522
- import { link, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, unlink } from "fs/promises";
4523
- import { basename as basename2, dirname, join as join4, resolve as resolve3 } from "path";
4524
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
4525
- import { Readable as Readable2, Transform } from "stream";
4526
- import { pipeline as pipeline2 } from "stream/promises";
4819
+ import { Command as Command71 } from "commander";
4820
+ import { mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
4821
+ import { basename as basename3, dirname as dirname2, join as join5, posix as pathPosix2, resolve as resolve3 } from "path";
4822
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
4527
4823
 
4528
4824
  // src/handwritten/commands/drive/decision.ts
4529
4825
  function decideDriveAction(entry, local, remote) {
@@ -4645,6 +4941,141 @@ function resolveInsideRoot(root, drivePath) {
4645
4941
  return absolutePath;
4646
4942
  }
4647
4943
 
4944
+ // src/handwritten/commands/drive/manifest.ts
4945
+ function normalizeRemoteManifest(root, entries) {
4946
+ const candidates = [];
4947
+ const remoteFiles = {};
4948
+ const pathErrors = [];
4949
+ for (const entry of entries) {
4950
+ try {
4951
+ validateDrivePath(entry.path);
4952
+ resolveInsideRoot(root, entry.path);
4953
+ candidates.push(entry);
4954
+ } catch (error) {
4955
+ pathErrors.push({ path: entry.path, error: error instanceof Error ? error : new Error(String(error)) });
4956
+ }
4957
+ }
4958
+ const byCaseFoldedPath = /* @__PURE__ */ new Map();
4959
+ for (const entry of candidates) {
4960
+ const folded = entry.path.toLowerCase();
4961
+ const group = byCaseFoldedPath.get(folded) ?? [];
4962
+ group.push(entry);
4963
+ byCaseFoldedPath.set(folded, group);
4964
+ }
4965
+ for (const group of byCaseFoldedPath.values()) {
4966
+ if (group.length > 1) {
4967
+ const hasExactDuplicate = new Set(group.map((entry2) => entry2.path)).size < group.length;
4968
+ const reason = hasExactDuplicate ? "REMOTE_PATH_DUPLICATE" : "REMOTE_PATH_CASE_CONFLICT";
4969
+ for (const entry2 of group.sort((left, right) => left.path.localeCompare(right.path))) {
4970
+ pathErrors.push({
4971
+ path: entry2.path,
4972
+ error: new Error(`${reason}: ${entry2.path}`),
4973
+ appendPathResult: true
4974
+ });
4975
+ }
4976
+ continue;
4977
+ }
4978
+ const [entry] = group;
4979
+ if (entry) remoteFiles[entry.path] = entry;
4980
+ }
4981
+ return { remoteFiles, pathErrors };
4982
+ }
4983
+
4984
+ // src/handwritten/commands/drive/merge.ts
4985
+ import { posix as pathPosix } from "path";
4986
+ import { TextDecoder } from "util";
4987
+ import { diff3Merge } from "node-diff3";
4988
+ var MAX_MERGE_TEXT_BYTES = 1024 * 1024;
4989
+ var SNIFF_BYTES = 8192;
4990
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
4991
+ ".txt",
4992
+ ".md",
4993
+ ".markdown",
4994
+ ".json",
4995
+ ".yaml",
4996
+ ".yml",
4997
+ ".csv",
4998
+ ".tsv",
4999
+ ".html",
5000
+ ".htm",
5001
+ ".css",
5002
+ ".js",
5003
+ ".jsx",
5004
+ ".ts",
5005
+ ".tsx",
5006
+ ".xml",
5007
+ ".svg"
5008
+ ]);
5009
+ var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
5010
+ function classifyMergeText(path, bytes, mimeType) {
5011
+ if (bytes.byteLength > MAX_MERGE_TEXT_BYTES) {
5012
+ return { mergeable: false, reason: "too_large" };
5013
+ }
5014
+ if (hasBinaryControlBytes(bytes)) {
5015
+ return { mergeable: false, reason: "binary" };
5016
+ }
5017
+ let text;
5018
+ try {
5019
+ text = UTF8_DECODER.decode(bytes);
5020
+ } catch {
5021
+ return { mergeable: false, reason: "invalid_utf8" };
5022
+ }
5023
+ if (!hasTextHint(path, mimeType)) {
5024
+ return { mergeable: false, reason: "not_text" };
5025
+ }
5026
+ return { mergeable: true, text };
5027
+ }
5028
+ function mergeText3(base, local, remote) {
5029
+ const localNewline = local.includes("\r\n") ? "\r\n" : "\n";
5030
+ const regions = diff3Merge(normalizeLines(local), normalizeLines(base), normalizeLines(remote));
5031
+ const mergedLines = [];
5032
+ for (const region of regions) {
5033
+ if (region.conflict !== void 0) {
5034
+ return { clean: false };
5035
+ }
5036
+ if (region.ok !== void 0) {
5037
+ mergedLines.push(...region.ok);
5038
+ }
5039
+ }
5040
+ return { clean: true, text: mergedLines.join(localNewline) };
5041
+ }
5042
+ function conflictCopyPath(path, side, timestamp, versionId) {
5043
+ const parsed = pathPosix.parse(path);
5044
+ const shortVersionId = safeShortVersionId(versionId);
5045
+ const fileName = `${parsed.name}.${side}-conflict-${timestamp}-${shortVersionId}${parsed.ext}`;
5046
+ if (parsed.dir === "") {
5047
+ return fileName;
5048
+ }
5049
+ return pathPosix.join(parsed.dir, fileName);
5050
+ }
5051
+ function hasBinaryControlBytes(bytes) {
5052
+ const sniffLength = Math.min(bytes.byteLength, SNIFF_BYTES);
5053
+ for (let index = 0; index < sniffLength; index += 1) {
5054
+ const byte = bytes[index];
5055
+ if (byte === void 0) {
5056
+ continue;
5057
+ }
5058
+ if (byte === 0) {
5059
+ return true;
5060
+ }
5061
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
5062
+ return true;
5063
+ }
5064
+ }
5065
+ return false;
5066
+ }
5067
+ function hasTextHint(path, mimeType) {
5068
+ const extension = pathPosix.extname(path).toLowerCase();
5069
+ return TEXT_EXTENSIONS.has(extension) || mimeType?.toLowerCase().startsWith("text/") === true;
5070
+ }
5071
+ function normalizeLines(text) {
5072
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
5073
+ }
5074
+ function safeShortVersionId(versionId) {
5075
+ const safeVersionId = versionId.replace(/[^A-Za-z0-9_-]/g, "_");
5076
+ return (safeVersionId.length > 0 ? safeVersionId : "unknown").slice(0, 8);
5077
+ }
5078
+
4648
5079
  // src/handwritten/commands/drive/scanner.ts
4649
5080
  import { createHash } from "crypto";
4650
5081
  import { constants as fsConstants } from "fs";
@@ -4722,7 +5153,7 @@ async function scanDriveFiles(root, options = {}) {
4722
5153
  }
4723
5154
  function isInternalSyncArtifactName(name) {
4724
5155
  if (!name.startsWith(".") || !name.endsWith(".tmp")) return false;
4725
- return name.includes(".wspc-download-") || name.includes(".wspc-backup-");
5156
+ return name.includes(".wspc-download-") || name.includes(".wspc-backup-") || name.includes(".wspc-conflict-") || name.includes(".wspc-merge-");
4726
5157
  }
4727
5158
  async function hashDriveFile(path) {
4728
5159
  const useNoFollow = fsConstants.O_NOFOLLOW !== void 0;
@@ -4765,236 +5196,128 @@ async function hashDriveFile(path) {
4765
5196
  }
4766
5197
  }
4767
5198
 
4768
- // src/handwritten/commands/drive/sync.ts
4769
- function emptySummary() {
4770
- return {
4771
- uploaded: 0,
4772
- downloaded: 0,
4773
- deleted: 0,
4774
- unchanged: 0,
4775
- conflicts: 0,
4776
- errors: 0,
4777
- paths: []
4778
- };
4779
- }
4780
- async function runDriveSyncOnce(root, api) {
4781
- return withDriveLock(root, async () => {
4782
- let state = await readDriveState(root);
4783
- const syncApi = api ?? await createDriveApi();
4784
- const summary = emptySummary();
4785
- const blockedPaths = /* @__PURE__ */ new Set();
4786
- const localFiles = await scanDriveFiles(root, {
4787
- onPathError: async (path, error) => {
4788
- await recordPathError(summary, blockedPaths, path, error);
4789
- }
4790
- });
4791
- const remoteFiles = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths);
4792
- const paths = Array.from(
4793
- /* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
4794
- ).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
4795
- for (const path of paths) {
4796
- const remote = remoteFiles[path];
4797
- const action = decideDriveAction(state.entries[path], localFiles[path], remote);
4798
- const result = await processPath({ root, state, api: syncApi, path, action, remote, local: localFiles[path], summary });
4799
- state = result.state;
4800
- if (result.stop) break;
4801
- }
4802
- recordUnresolvedConflicts(summary, state);
4803
- return summary;
5199
+ // src/handwritten/commands/drive/local-mutations.ts
5200
+ import { createWriteStream as createWriteStream2 } from "fs";
5201
+ import { link, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, unlink, writeFile as writeFile2 } from "fs/promises";
5202
+ import { basename as basename2, dirname, join as join4 } from "path";
5203
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
5204
+ import { Readable as Readable2, Transform } from "stream";
5205
+ import { pipeline as pipeline2 } from "stream/promises";
5206
+ async function assertLocalStillScanned(localPath, scanned) {
5207
+ const snapshot = await hashDriveFile(localPath).catch((error) => {
5208
+ if (isNotFoundError(error)) return void 0;
5209
+ throw error;
4804
5210
  });
5211
+ if (!snapshot || snapshot.sha256 !== scanned.sha256 || snapshot.sizeBytes !== scanned.size_bytes) {
5212
+ throw new Error("local file changed after scan");
5213
+ }
4805
5214
  }
4806
- function driveSyncCommand(api) {
4807
- const sync = new Command64("sync").description("Drive sync commands");
4808
- sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
4809
- const summary = await runDriveSyncOnce(resolve3(path), api);
4810
- render({ kind: "drive_sync_once", display: { shape: "object" } }, summary);
4811
- if (summary.conflicts > 0 || summary.errors > 0) {
4812
- process.exitCode = 1;
4813
- }
4814
- });
4815
- return sync;
5215
+ async function writeMergedLocalFile(root, path, bytes, digest, scanned, onLocalMutation) {
5216
+ const target = resolveInsideRoot(root, path);
5217
+ await mkdir2(dirname(target), { recursive: true });
5218
+ const tmp = join4(dirname(target), `.${basename2(target)}.wspc-merge-${randomUUID2()}.tmp`);
5219
+ try {
5220
+ await writeFile2(tmp, bytes, { flag: "wx" });
5221
+ return await installMergedLocalFile(root, path, tmp, scanned, digest, bytes.byteLength, onLocalMutation);
5222
+ } finally {
5223
+ await rm2(tmp, { force: true }).catch(() => {
5224
+ });
5225
+ }
4816
5226
  }
4817
- async function fetchRemoteManifest(root, state, api, summary, blockedPaths) {
4818
- const candidates = [];
4819
- const remoteFiles = {};
4820
- let cursor;
4821
- do {
4822
- const page = await api.getManifest(state.library_id, cursor);
4823
- for (const entry of page.entries) {
4824
- try {
4825
- validateRemoteEntry(root, entry);
4826
- candidates.push(entry);
4827
- } catch (error) {
4828
- await recordPathError(summary, blockedPaths, entry.path, error);
5227
+ async function installMergedLocalFile(root, path, tmp, scanned, mergedSha256, mergedSizeBytes, onLocalMutation) {
5228
+ const target = resolveInsideRoot(root, path);
5229
+ const backup = localMutationBackupPath(target);
5230
+ let backupIsScannedLocal = false;
5231
+ try {
5232
+ try {
5233
+ await rename2(target, backup);
5234
+ onLocalMutation();
5235
+ } catch (error) {
5236
+ if (isNotFoundError(error)) {
5237
+ throw new Error("local file changed after scan");
4829
5238
  }
5239
+ throw error;
4830
5240
  }
4831
- cursor = page.next_cursor ?? void 0;
4832
- } while (cursor !== void 0);
4833
- const byCaseFoldedPath = /* @__PURE__ */ new Map();
4834
- for (const entry of candidates) {
4835
- const folded = entry.path.toLowerCase();
4836
- const group = byCaseFoldedPath.get(folded) ?? [];
4837
- group.push(entry);
4838
- byCaseFoldedPath.set(folded, group);
4839
- }
4840
- for (const group of byCaseFoldedPath.values()) {
4841
- const exactPathCounts = /* @__PURE__ */ new Map();
4842
- for (const entry2 of group) {
4843
- exactPathCounts.set(entry2.path, (exactPathCounts.get(entry2.path) ?? 0) + 1);
5241
+ const backupDigest = await hashDriveFile(backup);
5242
+ if (!backupDigest || backupDigest.sha256 !== scanned.sha256 || backupDigest.sizeBytes !== scanned.size_bytes) {
5243
+ await restoreBackupWhenPossible(backup, target);
5244
+ throw new Error("local file changed after scan");
4844
5245
  }
4845
- if (group.length > 1) {
4846
- const hasExactDuplicate = Array.from(exactPathCounts.values()).some((count) => count > 1);
4847
- const reason = hasExactDuplicate ? "REMOTE_PATH_DUPLICATE" : "REMOTE_PATH_CASE_CONFLICT";
4848
- for (const entry2 of group.sort((left, right) => left.path.localeCompare(right.path))) {
4849
- await recordPathError(summary, blockedPaths, entry2.path, new Error(`${reason}: ${entry2.path}`), {
4850
- appendPathResult: true
5246
+ backupIsScannedLocal = true;
5247
+ try {
5248
+ await installNoOverwrite(tmp, target);
5249
+ } catch (error) {
5250
+ await restoreBackupWhenPossible(backup, target);
5251
+ throw error;
5252
+ }
5253
+ return {
5254
+ finalize: async () => {
5255
+ await rm2(backup, { force: true }).catch(() => {
4851
5256
  });
5257
+ },
5258
+ restore: async () => {
5259
+ await restoreMergedLocalFile(target, backup, scanned.sha256, scanned.size_bytes, mergedSha256, mergedSizeBytes);
4852
5260
  }
4853
- continue;
5261
+ };
5262
+ } catch (error) {
5263
+ if (!backupIsScannedLocal) {
5264
+ await restoreBackupWhenPossible(backup, target);
4854
5265
  }
4855
- const [entry] = group;
4856
- if (entry) remoteFiles[entry.path] = entry;
5266
+ throw error;
4857
5267
  }
4858
- return remoteFiles;
4859
5268
  }
4860
- function validateRemoteEntry(root, entry) {
4861
- validateDrivePath(entry.path);
4862
- resolveInsideRoot(root, entry.path);
5269
+ async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBytes, mergedSha256, mergedSizeBytes) {
5270
+ const quarantine = join4(dirname(target), `.${basename2(target)}.wspc-merge-restore-${randomUUID2()}.tmp`);
5271
+ try {
5272
+ await rename2(target, quarantine);
5273
+ } catch (error) {
5274
+ if (!isNotFoundError(error)) throw error;
5275
+ }
5276
+ const quarantineDigest = await hashDriveFile(quarantine).catch((error) => {
5277
+ if (isNotFoundError(error)) return void 0;
5278
+ throw error;
5279
+ });
5280
+ if (quarantineDigest !== void 0 && (quarantineDigest.sha256 !== mergedSha256 || quarantineDigest.sizeBytes !== mergedSizeBytes)) {
5281
+ await restoreBackupWhenPossible(quarantine, target);
5282
+ return;
5283
+ }
5284
+ const restored = await restoreBackupWhenPossible(backup, target);
5285
+ if (!restored) {
5286
+ await rm2(quarantine, { force: true }).catch(() => {
5287
+ });
5288
+ return;
5289
+ }
5290
+ await unlink(quarantine).catch(() => {
5291
+ });
5292
+ const restoredDigest = await hashDriveFile(target);
5293
+ if (!restoredDigest || restoredDigest.sha256 !== backupSha256 || restoredDigest.sizeBytes !== backupSizeBytes) {
5294
+ throw new Error("local file restore failed");
5295
+ }
4863
5296
  }
4864
- async function processPath(args) {
4865
- const { root, state, api, path, action, remote, local, summary } = args;
4866
- summary.paths.push({ path, action: action.type });
4867
- let durableStateRequired = false;
5297
+ async function downloadRemote(root, libraryId, path, api, expectedSha256, entry, onLocalMutation) {
5298
+ const target = resolveInsideRoot(root, path);
5299
+ await mkdir2(dirname(target), { recursive: true });
5300
+ const tmp = join4(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID2()}.tmp`);
4868
5301
  try {
4869
- if (action.type === "upload_create" || action.type === "upload_update") {
4870
- const localPath = resolveInsideRoot(root, path);
4871
- const { body, digest: uploadDigest } = await readStableUploadBody(localPath, local);
4872
- const uploaded = await api.uploadFile(state.library_id, path, body, uploadDigest, action.expectedEntryVersion);
4873
- durableStateRequired = true;
4874
- const nextState = cloneDriveState(state);
4875
- nextState.entries[path] = stateEntryFromRemote(uploaded.entry, uploadDigest);
4876
- delete nextState.conflicts[path];
4877
- await commitDriveState(root, nextState);
4878
- summary.uploaded += 1;
4879
- return { state: nextState, stop: false };
4880
- }
4881
- if (action.type === "download") {
4882
- if (!remote) throw new Error("remote entry missing for download");
4883
- await assertLocalSafeForDownload(root, path, state.entries[path]);
4884
- const digest = await downloadRemote(root, state.library_id, path, api, remote.content_sha256, state.entries[path], () => {
4885
- durableStateRequired = true;
4886
- });
4887
- const nextState = cloneDriveState(state);
4888
- nextState.entries[path] = stateEntryFromRemote(remote, digest);
4889
- delete nextState.conflicts[path];
4890
- await commitDriveState(root, nextState);
4891
- summary.downloaded += 1;
4892
- return { state: nextState, stop: false };
5302
+ const response = await api.downloadFile(libraryId, path);
5303
+ if (!response.body) {
5304
+ throw new Error("download response body missing");
4893
5305
  }
4894
- if (action.type === "delete_remote") {
4895
- await assertLocalAbsentBeforeRemoteDelete(root, path);
4896
- await api.deleteFile(state.library_id, path, action.expectedEntryVersion);
4897
- durableStateRequired = true;
4898
- await assertLocalAbsentBeforeRemoteDelete(root, path);
4899
- const nextState = cloneDriveState(state);
4900
- delete nextState.entries[path];
4901
- delete nextState.conflicts[path];
4902
- await commitDriveState(root, nextState);
4903
- summary.deleted += 1;
4904
- return { state: nextState, stop: false };
4905
- }
4906
- if (action.type === "delete_local") {
4907
- await removeLocalIfStillBase(root, path, state.entries[path], () => {
4908
- durableStateRequired = true;
4909
- });
4910
- const nextState = cloneDriveState(state);
4911
- delete nextState.entries[path];
4912
- delete nextState.conflicts[path];
4913
- await commitDriveState(root, nextState);
4914
- summary.deleted += 1;
4915
- return { state: nextState, stop: false };
4916
- }
4917
- if (action.type === "state_only") {
4918
- if (!remote) throw new Error("remote entry missing for state update");
4919
- const nextState = cloneDriveState(state);
4920
- nextState.entries[path] = stateEntryFromRemote(remote, local?.sha256 ?? remote.content_sha256);
4921
- delete nextState.conflicts[path];
4922
- await commitDriveState(root, nextState);
4923
- summary.unchanged += 1;
4924
- return { state: nextState, stop: false };
4925
- }
4926
- if (action.type === "remove_state") {
4927
- const nextState = cloneDriveState(state);
4928
- delete nextState.entries[path];
4929
- delete nextState.conflicts[path];
4930
- await commitDriveState(root, nextState);
4931
- summary.unchanged += 1;
4932
- return { state: nextState, stop: false };
4933
- }
4934
- if (action.type === "conflict") {
4935
- const nextState = await recordConflict(root, state, path, action.reason, remote);
4936
- summary.conflicts += 1;
4937
- return { state: nextState, stop: false };
4938
- }
4939
- summary.unchanged += 1;
4940
- } catch (error) {
4941
- if (isVersionConflict(error)) {
4942
- try {
4943
- const nextState = await recordConflict(root, state, path, "VERSION_CONFLICT", remote);
4944
- summary.conflicts += 1;
4945
- summary.paths[summary.paths.length - 1] = { path, action: "conflict" };
4946
- return { state: nextState, stop: false };
4947
- } catch (writeError) {
4948
- await recordPathError(summary, void 0, path, writeError);
4949
- return { state, stop: durableStateRequired };
4950
- }
4951
- }
4952
- await recordPathError(summary, void 0, path, error);
4953
- return { state, stop: durableStateRequired };
4954
- }
4955
- return { state, stop: false };
4956
- }
4957
- function recordUnresolvedConflicts(summary, state) {
4958
- const newlyRecorded = new Set(summary.paths.filter((result) => result.action === "conflict").map((result) => result.path));
4959
- const reportedPaths = new Set(summary.paths.map((result) => result.path));
4960
- for (const path of Object.keys(state.conflicts).sort((left, right) => left.localeCompare(right))) {
4961
- if (!newlyRecorded.has(path)) {
4962
- summary.conflicts += 1;
4963
- }
4964
- const existingResult = summary.paths.find((result) => result.path === path);
4965
- if (existingResult?.action === "unchanged") {
4966
- existingResult.action = "conflict";
4967
- continue;
4968
- }
4969
- if (!reportedPaths.has(path)) {
4970
- summary.paths.push({ path, action: "conflict" });
4971
- }
4972
- }
4973
- }
4974
- async function downloadRemote(root, libraryId, path, api, expectedSha256, entry, onLocalMutation) {
4975
- const target = resolveInsideRoot(root, path);
4976
- await mkdir2(dirname(target), { recursive: true });
4977
- const tmp = join4(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID2()}.tmp`);
4978
- try {
4979
- const response = await api.downloadFile(libraryId, path);
4980
- if (!response.body) {
4981
- throw new Error("download response body missing");
4982
- }
4983
- const hash = createHash2("sha256");
4984
- const hashingStream = new Transform({
4985
- transform(chunk, _encoding, callback) {
4986
- hash.update(chunk);
4987
- callback(void 0, chunk);
4988
- }
4989
- });
4990
- await pipeline2(
4991
- Readable2.fromWeb(response.body),
4992
- hashingStream,
4993
- createWriteStream2(tmp, { flags: "wx" })
4994
- );
4995
- const digest = hash.digest("hex");
4996
- if (expectedSha256 !== void 0 && digest !== expectedSha256) {
4997
- throw new Error(`download hash mismatch: expected ${expectedSha256}, got ${digest}`);
5306
+ const hash = createHash2("sha256");
5307
+ const hashingStream = new Transform({
5308
+ transform(chunk, _encoding, callback) {
5309
+ hash.update(chunk);
5310
+ callback(void 0, chunk);
5311
+ }
5312
+ });
5313
+ await pipeline2(
5314
+ Readable2.fromWeb(response.body),
5315
+ hashingStream,
5316
+ createWriteStream2(tmp, { flags: "wx" })
5317
+ );
5318
+ const digest = hash.digest("hex");
5319
+ if (expectedSha256 !== void 0 && digest !== expectedSha256) {
5320
+ throw new Error(`download hash mismatch: expected ${expectedSha256}, got ${digest}`);
4998
5321
  }
4999
5322
  await installDownloadedFile(root, path, tmp, entry, onLocalMutation);
5000
5323
  return digest;
@@ -5162,7 +5485,431 @@ async function assertLocalAbsentBeforeRemoteDelete(root, path) {
5162
5485
  throw new Error("local file appeared before remote delete");
5163
5486
  }
5164
5487
  }
5165
- function stateEntryFromRemote(remote, localSha256) {
5488
+ function isNotFoundError(error) {
5489
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
5490
+ }
5491
+ function isAlreadyExistsError(error) {
5492
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
5493
+ }
5494
+
5495
+ // src/handwritten/commands/drive/sync.ts
5496
+ function emptySummary() {
5497
+ return {
5498
+ uploaded: 0,
5499
+ downloaded: 0,
5500
+ deleted: 0,
5501
+ unchanged: 0,
5502
+ merged: 0,
5503
+ conflicts: 0,
5504
+ errors: 0,
5505
+ conflict_paths: [],
5506
+ paths: []
5507
+ };
5508
+ }
5509
+ async function runDriveSyncOnce(root, api, clock = systemDriveClock) {
5510
+ return withDriveLock(root, async () => {
5511
+ let state = await readDriveState(root);
5512
+ const syncApi = api ?? await createDriveApi();
5513
+ const summary = emptySummary();
5514
+ const blockedPaths = /* @__PURE__ */ new Set();
5515
+ const localFiles = await scanDriveFiles(root, {
5516
+ onPathError: async (path, error) => {
5517
+ await recordPathError(summary, blockedPaths, path, error);
5518
+ }
5519
+ });
5520
+ const remoteFiles = await fetchRemoteManifest(root, state, syncApi, summary, blockedPaths);
5521
+ const paths = Array.from(
5522
+ /* @__PURE__ */ new Set([...Object.keys(localFiles), ...Object.keys(remoteFiles), ...Object.keys(state.entries)])
5523
+ ).filter((path) => !blockedPaths.has(path)).sort((left, right) => left.localeCompare(right));
5524
+ for (const path of paths) {
5525
+ const remote = remoteFiles[path];
5526
+ const action = decideDriveAction(state.entries[path], localFiles[path], remote);
5527
+ const result = await processPath({ root, state, api: syncApi, path, action, remote, local: localFiles[path], summary, clock });
5528
+ state = result.state;
5529
+ if (result.stop) break;
5530
+ }
5531
+ recordUnresolvedConflicts(summary, state);
5532
+ return summary;
5533
+ });
5534
+ }
5535
+ function driveSyncCommand(api) {
5536
+ const sync = new Command71("sync").description("Drive sync commands");
5537
+ sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
5538
+ const summary = await runDriveSyncOnce(resolve3(path), api);
5539
+ render({ kind: "drive_sync_once", display: { shape: "object" } }, summary);
5540
+ if (summary.conflicts > 0 || summary.errors > 0) {
5541
+ process.exitCode = 1;
5542
+ }
5543
+ });
5544
+ return sync;
5545
+ }
5546
+ async function fetchRemoteManifest(root, state, api, summary, blockedPaths) {
5547
+ const entries = [];
5548
+ let cursor;
5549
+ do {
5550
+ const page = await api.getManifest(state.library_id, cursor);
5551
+ entries.push(...page.entries);
5552
+ cursor = page.next_cursor ?? void 0;
5553
+ } while (cursor !== void 0);
5554
+ const normalized = normalizeRemoteManifest(root, entries);
5555
+ for (const pathError of normalized.pathErrors) {
5556
+ await recordPathError(summary, blockedPaths, pathError.path, pathError.error, {
5557
+ appendPathResult: pathError.appendPathResult
5558
+ });
5559
+ }
5560
+ return normalized.remoteFiles;
5561
+ }
5562
+ async function processPath(args) {
5563
+ const { root, state, api, path, action, remote, local, summary, clock } = args;
5564
+ summary.paths.push({ path, action: action.type });
5565
+ let durableStateRequired = false;
5566
+ try {
5567
+ if (action.type === "upload_create" || action.type === "upload_update") {
5568
+ const localPath = resolveInsideRoot(root, path);
5569
+ const { body, digest: uploadDigest } = await readStableUploadBody(localPath, local);
5570
+ const uploaded = await api.uploadFile(state.library_id, path, body, uploadDigest, action.expectedEntryVersion);
5571
+ durableStateRequired = true;
5572
+ const nextState = cloneDriveState(state);
5573
+ nextState.entries[path] = stateEntryFromRemote(uploaded.entry, uploadDigest, clock);
5574
+ delete nextState.conflicts[path];
5575
+ await writeDriveState(root, nextState, clock);
5576
+ summary.uploaded += 1;
5577
+ return { state: nextState, stop: false };
5578
+ }
5579
+ if (action.type === "download") {
5580
+ if (!remote) throw new Error("remote entry missing for download");
5581
+ await assertLocalSafeForDownload(root, path, state.entries[path]);
5582
+ const digest = await downloadRemote(root, state.library_id, path, api, remote.content_sha256, state.entries[path], () => {
5583
+ durableStateRequired = true;
5584
+ });
5585
+ const nextState = cloneDriveState(state);
5586
+ nextState.entries[path] = stateEntryFromRemote(remote, digest, clock);
5587
+ delete nextState.conflicts[path];
5588
+ await writeDriveState(root, nextState, clock);
5589
+ summary.downloaded += 1;
5590
+ return { state: nextState, stop: false };
5591
+ }
5592
+ if (action.type === "delete_remote") {
5593
+ await assertLocalAbsentBeforeRemoteDelete(root, path);
5594
+ await api.deleteFile(state.library_id, path, action.expectedEntryVersion);
5595
+ durableStateRequired = true;
5596
+ await assertLocalAbsentBeforeRemoteDelete(root, path);
5597
+ const nextState = cloneDriveState(state);
5598
+ delete nextState.entries[path];
5599
+ delete nextState.conflicts[path];
5600
+ await writeDriveState(root, nextState, clock);
5601
+ summary.deleted += 1;
5602
+ return { state: nextState, stop: false };
5603
+ }
5604
+ if (action.type === "delete_local") {
5605
+ await removeLocalIfStillBase(root, path, state.entries[path], () => {
5606
+ durableStateRequired = true;
5607
+ });
5608
+ const nextState = cloneDriveState(state);
5609
+ delete nextState.entries[path];
5610
+ delete nextState.conflicts[path];
5611
+ await writeDriveState(root, nextState, clock);
5612
+ summary.deleted += 1;
5613
+ return { state: nextState, stop: false };
5614
+ }
5615
+ if (action.type === "state_only") {
5616
+ if (!remote) throw new Error("remote entry missing for state update");
5617
+ const nextState = cloneDriveState(state);
5618
+ nextState.entries[path] = stateEntryFromRemote(remote, local?.sha256 ?? remote.content_sha256, clock);
5619
+ delete nextState.conflicts[path];
5620
+ await writeDriveState(root, nextState, clock);
5621
+ summary.unchanged += 1;
5622
+ return { state: nextState, stop: false };
5623
+ }
5624
+ if (action.type === "remove_state") {
5625
+ const nextState = cloneDriveState(state);
5626
+ delete nextState.entries[path];
5627
+ delete nextState.conflicts[path];
5628
+ await writeDriveState(root, nextState, clock);
5629
+ summary.unchanged += 1;
5630
+ return { state: nextState, stop: false };
5631
+ }
5632
+ if (action.type === "conflict") {
5633
+ if (action.reason === "local_and_remote_changed") {
5634
+ try {
5635
+ const mergedState = await tryResolveConflict({
5636
+ root,
5637
+ state,
5638
+ api,
5639
+ path,
5640
+ remote,
5641
+ local,
5642
+ clock,
5643
+ onLocalMutation: () => {
5644
+ durableStateRequired = true;
5645
+ }
5646
+ });
5647
+ if (mergedState) {
5648
+ summary.merged += 1;
5649
+ summary.paths[summary.paths.length - 1] = { path, action: "merged" };
5650
+ return { state: mergedState, stop: false };
5651
+ }
5652
+ } catch (error) {
5653
+ if (!isLocalChangedDuringMerge(error)) {
5654
+ throw error;
5655
+ }
5656
+ const nextState2 = await recordTypedConflict(root, state, path, action.reason, remote, clock, {
5657
+ type: "edit_edit",
5658
+ strategy: "record_only",
5659
+ reason: "local_changed_during_merge"
5660
+ });
5661
+ summary.conflicts += 1;
5662
+ return { state: nextState2, stop: false };
5663
+ }
5664
+ const conflictCopyState = await recordRemoteConflictCopy({
5665
+ root,
5666
+ state,
5667
+ api,
5668
+ path,
5669
+ reason: action.reason,
5670
+ type: "edit_edit",
5671
+ remote,
5672
+ clock
5673
+ });
5674
+ if (conflictCopyState) {
5675
+ summary.conflicts += 1;
5676
+ return { state: conflictCopyState, stop: false };
5677
+ }
5678
+ }
5679
+ if (action.reason === "local_and_remote_without_base") {
5680
+ const conflictCopyState = await recordRemoteConflictCopy({
5681
+ root,
5682
+ state,
5683
+ api,
5684
+ path,
5685
+ reason: action.reason,
5686
+ type: "create_create",
5687
+ remote,
5688
+ clock
5689
+ });
5690
+ if (conflictCopyState) {
5691
+ summary.conflicts += 1;
5692
+ return { state: conflictCopyState, stop: false };
5693
+ }
5694
+ }
5695
+ if (action.reason === "local_changed_remote_deleted") {
5696
+ const nextState2 = await recordTypedConflict(root, state, path, action.reason, remote, clock, {
5697
+ type: "edit_delete",
5698
+ strategy: "record_only"
5699
+ });
5700
+ summary.conflicts += 1;
5701
+ return { state: nextState2, stop: false };
5702
+ }
5703
+ if (action.reason === "remote_changed_before_delete") {
5704
+ const conflictCopyState = await recordRemoteConflictCopy({
5705
+ root,
5706
+ state,
5707
+ api,
5708
+ path,
5709
+ reason: action.reason,
5710
+ type: "delete_edit",
5711
+ remote,
5712
+ clock
5713
+ });
5714
+ if (conflictCopyState) {
5715
+ summary.conflicts += 1;
5716
+ return { state: conflictCopyState, stop: false };
5717
+ }
5718
+ }
5719
+ const nextState = await recordConflict(root, state, path, action.reason, remote, clock);
5720
+ summary.conflicts += 1;
5721
+ return { state: nextState, stop: false };
5722
+ }
5723
+ summary.unchanged += 1;
5724
+ } catch (error) {
5725
+ if (isVersionConflict(error)) {
5726
+ try {
5727
+ const nextState = await recordConflict(root, state, path, "VERSION_CONFLICT", remote, clock);
5728
+ summary.conflicts += 1;
5729
+ summary.paths[summary.paths.length - 1] = { path, action: "conflict" };
5730
+ return { state: nextState, stop: false };
5731
+ } catch (writeError) {
5732
+ await recordPathError(summary, void 0, path, writeError);
5733
+ return { state, stop: durableStateRequired };
5734
+ }
5735
+ }
5736
+ await recordPathError(summary, void 0, path, error);
5737
+ return { state, stop: durableStateRequired };
5738
+ }
5739
+ return { state, stop: false };
5740
+ }
5741
+ async function tryResolveConflict(args) {
5742
+ const { root, state, api, path, remote, local, clock, onLocalMutation } = args;
5743
+ const entry = state.entries[path];
5744
+ const baseVersionId = entry?.current_version_id;
5745
+ const remoteVersionId = remote?.current_version_id;
5746
+ if (!entry || !remote || !local || baseVersionId === void 0 || remoteVersionId === void 0) {
5747
+ return void 0;
5748
+ }
5749
+ const localPath = resolveInsideRoot(root, path);
5750
+ let baseBytes;
5751
+ let remoteBytes;
5752
+ try {
5753
+ const [downloadedBaseBytes, downloadedRemoteBytes] = await Promise.all([
5754
+ downloadBytes(api, state.library_id, path, baseVersionId),
5755
+ downloadBytes(api, state.library_id, path, remoteVersionId)
5756
+ ]);
5757
+ baseBytes = downloadedBaseBytes;
5758
+ remoteBytes = downloadedRemoteBytes;
5759
+ } catch (error) {
5760
+ if (isExpectedVersionDownloadMissing(error)) {
5761
+ return void 0;
5762
+ }
5763
+ throw error;
5764
+ }
5765
+ const localBytes = await readFile4(localPath);
5766
+ const baseText = classifyMergeText(path, baseBytes, void 0);
5767
+ const localText = classifyMergeText(path, localBytes, void 0);
5768
+ const remoteText = classifyMergeText(path, remoteBytes, void 0);
5769
+ if (!baseText.mergeable || !localText.mergeable || !remoteText.mergeable) {
5770
+ return void 0;
5771
+ }
5772
+ const merged = mergeText3(baseText.text, localText.text, remoteText.text);
5773
+ if (!merged.clean) {
5774
+ return void 0;
5775
+ }
5776
+ await assertLocalStillScanned(localPath, local);
5777
+ const mergedBytes = new TextEncoder().encode(merged.text);
5778
+ const mergedDigest = createHash3("sha256").update(mergedBytes).digest("hex");
5779
+ const install = await writeMergedLocalFile(root, path, mergedBytes, mergedDigest, local, onLocalMutation);
5780
+ let uploaded;
5781
+ try {
5782
+ uploaded = await api.uploadFile(state.library_id, path, mergedBytes, mergedDigest, remote.entry_version);
5783
+ } catch (error) {
5784
+ await install.restore();
5785
+ throw error;
5786
+ }
5787
+ await install.finalize();
5788
+ const nextState = cloneDriveState(state);
5789
+ nextState.entries[path] = stateEntryFromRemote(uploaded.entry, mergedDigest, clock);
5790
+ delete nextState.conflicts[path];
5791
+ await writeDriveState(root, nextState, clock);
5792
+ return nextState;
5793
+ }
5794
+ async function downloadBytes(api, libraryId, path, versionId) {
5795
+ const response = await api.downloadFile(libraryId, path, versionId);
5796
+ return new Uint8Array(await response.arrayBuffer());
5797
+ }
5798
+ async function recordRemoteConflictCopy(args) {
5799
+ const { root, state, api, path, reason, type, remote, clock } = args;
5800
+ const entry = state.entries[path];
5801
+ const remoteVersionId = remote?.current_version_id;
5802
+ if (!remote || remoteVersionId === void 0) {
5803
+ return void 0;
5804
+ }
5805
+ if (await canReuseConflictCopy(root, state.conflicts[path], remoteVersionId)) {
5806
+ return state;
5807
+ }
5808
+ const remoteBytes = await downloadBytes(api, state.library_id, path, remoteVersionId);
5809
+ const copyPath = await writeConflictCopy(root, path, "remote", remoteVersionId, remoteBytes, clock);
5810
+ const nextState = cloneDriveState(state);
5811
+ nextState.conflicts[path] = {
5812
+ detected_at: driveIsoTimestamp(clock),
5813
+ reason,
5814
+ type,
5815
+ strategy: "conflict_copy",
5816
+ base_version_id: entry?.current_version_id,
5817
+ remote_version_id: remoteVersionId,
5818
+ remote_entry_version: remote.entry_version,
5819
+ conflict_paths: [copyPath]
5820
+ };
5821
+ await writeDriveState(root, nextState, clock);
5822
+ return nextState;
5823
+ }
5824
+ async function canReuseConflictCopy(root, conflict2, remoteVersionId) {
5825
+ if (conflict2?.strategy !== "conflict_copy" || conflict2.remote_version_id !== remoteVersionId || !Array.isArray(conflict2.conflict_paths) || conflict2.conflict_paths.length === 0) {
5826
+ return false;
5827
+ }
5828
+ for (const conflictPath of conflict2.conflict_paths) {
5829
+ try {
5830
+ validateDrivePath(conflictPath);
5831
+ if (!await localFileExists(resolveInsideRoot(root, conflictPath))) {
5832
+ return false;
5833
+ }
5834
+ } catch {
5835
+ return false;
5836
+ }
5837
+ }
5838
+ return true;
5839
+ }
5840
+ async function writeConflictCopy(root, path, side, versionId, bytes, clock) {
5841
+ const baseCopyPath = conflictCopyPath(path, side, driveConflictTimestamp(clock), versionId);
5842
+ for (let suffix = 1; ; suffix += 1) {
5843
+ const candidate = conflictCopyPathWithSuffix(baseCopyPath, suffix);
5844
+ validateDrivePath(candidate);
5845
+ const target = resolveInsideRoot(root, candidate);
5846
+ await mkdir3(dirname2(target), { recursive: true });
5847
+ for (; ; ) {
5848
+ const tmp = join5(dirname2(target), `.${basename3(target)}.wspc-conflict-${randomUUID3()}.tmp`);
5849
+ let tmpWritten = false;
5850
+ try {
5851
+ await writeFile3(tmp, bytes, { flag: "wx" });
5852
+ tmpWritten = true;
5853
+ await installNoOverwrite(tmp, target);
5854
+ return candidate;
5855
+ } catch (error) {
5856
+ if (tmpWritten) {
5857
+ await rm3(tmp, { force: true }).catch(() => {
5858
+ });
5859
+ }
5860
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
5861
+ if (tmpWritten) break;
5862
+ continue;
5863
+ }
5864
+ throw error;
5865
+ }
5866
+ }
5867
+ }
5868
+ }
5869
+ function conflictCopyPathWithSuffix(path, suffix) {
5870
+ if (suffix === 1) {
5871
+ return path;
5872
+ }
5873
+ const parsed = pathPosix2.parse(path);
5874
+ const fileName = `${parsed.name}-${suffix}${parsed.ext}`;
5875
+ if (parsed.dir === "") {
5876
+ return fileName;
5877
+ }
5878
+ return pathPosix2.join(parsed.dir, fileName);
5879
+ }
5880
+ function isExpectedVersionDownloadMissing(error) {
5881
+ const structured = error;
5882
+ if (structured?.status === 404 || structured?.status === 410) return true;
5883
+ if (structured?.response?.status === 404 || structured?.response?.status === 410) return true;
5884
+ if (structured?.code === "VERSION_NOT_FOUND" || structured?.code === "NOT_FOUND") return true;
5885
+ return /\b(?:HTTP 40[410]|missing version|version not found|not found)\b/i.test(errorMessage(error));
5886
+ }
5887
+ function recordUnresolvedConflicts(summary, state) {
5888
+ const newlyRecorded = new Set(summary.paths.filter((result) => result.action === "conflict").map((result) => result.path));
5889
+ const reportedPaths = new Set(summary.paths.map((result) => result.path));
5890
+ for (const path of Object.keys(state.conflicts).sort((left, right) => left.localeCompare(right))) {
5891
+ const conflictPaths = state.conflicts[path]?.conflict_paths;
5892
+ if (conflictPaths) {
5893
+ summary.conflict_paths.push(...conflictPaths);
5894
+ }
5895
+ if (!newlyRecorded.has(path)) {
5896
+ summary.conflicts += 1;
5897
+ }
5898
+ const existingResult = summary.paths.find((result) => result.path === path);
5899
+ if (existingResult?.action === "unchanged") {
5900
+ existingResult.action = "conflict";
5901
+ if (conflictPaths) existingResult.conflict_paths = conflictPaths;
5902
+ continue;
5903
+ }
5904
+ if (existingResult?.action === "conflict" && conflictPaths) {
5905
+ existingResult.conflict_paths = conflictPaths;
5906
+ }
5907
+ if (!reportedPaths.has(path)) {
5908
+ summary.paths.push({ path, action: "conflict", ...conflictPaths ? { conflict_paths: conflictPaths } : {} });
5909
+ }
5910
+ }
5911
+ }
5912
+ function stateEntryFromRemote(remote, localSha256, clock) {
5166
5913
  return {
5167
5914
  entry_id: remote.id,
5168
5915
  entry_version: remote.entry_version,
@@ -5170,13 +5917,10 @@ function stateEntryFromRemote(remote, localSha256) {
5170
5917
  content_sha256: remote.content_sha256,
5171
5918
  size_bytes: remote.size_bytes,
5172
5919
  last_local_sha256: localSha256,
5173
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
5920
+ last_synced_at: driveIsoTimestamp(clock),
5174
5921
  status: "synced"
5175
5922
  };
5176
5923
  }
5177
- async function commitDriveState(root, nextState) {
5178
- await writeDriveState(root, nextState);
5179
- }
5180
5924
  function cloneDriveState(state) {
5181
5925
  return {
5182
5926
  ...state,
@@ -5184,10 +5928,21 @@ function cloneDriveState(state) {
5184
5928
  conflicts: { ...state.conflicts }
5185
5929
  };
5186
5930
  }
5187
- async function recordConflict(root, state, path, reason, remote) {
5931
+ async function recordConflict(root, state, path, reason, remote, clock) {
5188
5932
  const nextState = cloneDriveState(state);
5189
- nextState.conflicts[path] = conflict(reason, remote);
5190
- await commitDriveState(root, nextState);
5933
+ nextState.conflicts[path] = conflict(reason, remote, clock);
5934
+ await writeDriveState(root, nextState, clock);
5935
+ return nextState;
5936
+ }
5937
+ async function recordTypedConflict(root, state, path, reason, remote, clock, metadata) {
5938
+ const nextState = cloneDriveState(state);
5939
+ nextState.conflicts[path] = {
5940
+ ...conflict(metadata.reason ?? reason, remote, clock),
5941
+ type: metadata.type,
5942
+ strategy: metadata.strategy,
5943
+ base_version_id: state.entries[path]?.current_version_id
5944
+ };
5945
+ await writeDriveState(root, nextState, clock);
5191
5946
  return nextState;
5192
5947
  }
5193
5948
  async function recordPathError(summary, blockedPaths, path, error, options = {}) {
@@ -5201,9 +5956,9 @@ async function recordPathError(summary, blockedPaths, path, error, options = {})
5201
5956
  void errorMessage(error);
5202
5957
  summary.errors += 1;
5203
5958
  }
5204
- function conflict(reason, remote) {
5959
+ function conflict(reason, remote, clock) {
5205
5960
  return {
5206
- detected_at: (/* @__PURE__ */ new Date()).toISOString(),
5961
+ detected_at: driveIsoTimestamp(clock),
5207
5962
  reason,
5208
5963
  remote_entry_version: remote?.entry_version,
5209
5964
  remote_version_id: remote?.current_version_id
@@ -5217,6 +5972,9 @@ function isVersionConflict(error) {
5217
5972
  if (structured?.code === "VERSION_CONFLICT") return true;
5218
5973
  return [errorMessage(error), structured?.body, structured?.response?.body].some(containsVersionConflict);
5219
5974
  }
5975
+ function isLocalChangedDuringMerge(error) {
5976
+ return errorMessage(error) === "local file changed after scan";
5977
+ }
5220
5978
  function containsVersionConflict(value) {
5221
5979
  if (value === void 0) return false;
5222
5980
  if (typeof value === "string") return value.includes("VERSION_CONFLICT");
@@ -5226,22 +5984,268 @@ function containsVersionConflict(value) {
5226
5984
  return false;
5227
5985
  }
5228
5986
  }
5229
- function isNotFoundError(error) {
5230
- return error instanceof Error && "code" in error && error.code === "ENOENT";
5231
- }
5232
- function isAlreadyExistsError(error) {
5233
- return error instanceof Error && "code" in error && error.code === "EEXIST";
5234
- }
5235
5987
 
5236
5988
  // src/handwritten/commands/drive/watch.ts
5237
- import { Command as Command65 } from "commander";
5989
+ import { Command as Command72 } from "commander";
5238
5990
  import chokidar from "chokidar";
5239
5991
  import { relative as relative2, resolve as resolve4 } from "path";
5992
+
5993
+ // src/handwritten/commands/drive/realtime.ts
5994
+ function createDriveRealtimeSource(args) {
5995
+ const connect = args.connect ?? nativeWebSocketConnector;
5996
+ const clock = args.clock ?? systemDriveClock;
5997
+ const scheduleTimeout = args.setTimeout ?? setTimeout;
5998
+ const cancelTimeout = args.clearTimeout ?? clearTimeout;
5999
+ let currentRealtime = { ...args.realtime };
6000
+ let handlers;
6001
+ let activeSocket;
6002
+ let reconnectTimer;
6003
+ let reconnectDelayMs = 1e3;
6004
+ let stopped = false;
6005
+ let authFailed = false;
6006
+ let connectionId = 0;
6007
+ function clearReconnectTimer() {
6008
+ if (reconnectTimer === void 0) return;
6009
+ cancelTimeout(reconnectTimer);
6010
+ reconnectTimer = void 0;
6011
+ }
6012
+ async function persist(next) {
6013
+ currentRealtime = next;
6014
+ await args.writeRealtimeState(next);
6015
+ }
6016
+ async function persistBestEffort(next) {
6017
+ try {
6018
+ await persist(next);
6019
+ } catch (error) {
6020
+ handlers?.onWarning?.(redactedRealtimeError(error));
6021
+ }
6022
+ }
6023
+ function connectNow() {
6024
+ if (stopped || authFailed) return;
6025
+ clearReconnectTimer();
6026
+ const id = ++connectionId;
6027
+ const url = buildDriveRealtimeUrl(args.baseUrl, args.libraryId, currentRealtime);
6028
+ activeSocket = connect(url, {
6029
+ open() {
6030
+ if (id !== connectionId || stopped || authFailed) return;
6031
+ reconnectDelayMs = 1e3;
6032
+ void persistBestEffort({ ...currentRealtime, last_connected_at: driveIsoTimestamp(clock) }).then(() => handlers?.onConnected());
6033
+ },
6034
+ message(data) {
6035
+ if (id !== connectionId || stopped || authFailed) return;
6036
+ void handleMessage(data).catch((error) => handlers?.onWarning?.(redactedRealtimeError(error)));
6037
+ },
6038
+ close(error) {
6039
+ closeConnection(id, error ?? "close");
6040
+ }
6041
+ }, args.headers === void 0 ? void 0 : { headers: args.headers });
6042
+ }
6043
+ function closeConnection(id, error) {
6044
+ if (id !== connectionId || stopped || authFailed) return;
6045
+ connectionId += 1;
6046
+ const socket = activeSocket;
6047
+ activeSocket = void 0;
6048
+ clearReconnectTimer();
6049
+ socket?.close();
6050
+ if (isRealtimeAuthError(error)) {
6051
+ authFailed = true;
6052
+ handlers?.onAuthFailed(redactedRealtimeError(error));
6053
+ return;
6054
+ }
6055
+ const delayMs = reconnectDelayMs;
6056
+ handlers?.onReconnect(delayMs, redactedRealtimeError(error));
6057
+ reconnectTimer = scheduleTimeout(() => {
6058
+ reconnectTimer = void 0;
6059
+ connectNow();
6060
+ }, delayMs);
6061
+ reconnectDelayMs = Math.min(delayMs * 2, 6e4);
6062
+ }
6063
+ async function handleMessage(data) {
6064
+ const message = parseDriveRealtimeMessage(data);
6065
+ if (message.type === "ready") {
6066
+ if (message.replayed > 0) {
6067
+ handlers?.onEvent(optionalString({ debounce_ms: 2e3, reason: "ready_replay" }, "cursor", message.cursor));
6068
+ }
6069
+ if (message.cursor !== void 0) {
6070
+ await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor });
6071
+ }
6072
+ return;
6073
+ }
6074
+ if (message.type === "library_changed") {
6075
+ handlers?.onEvent(optionalString(optionalString({
6076
+ debounce_ms: 2e3,
6077
+ reason: "library_changed"
6078
+ }, "cursor", message.cursor), "path", message.path));
6079
+ if (message.cursor !== void 0) {
6080
+ await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor, last_event_at: driveIsoTimestamp(clock) });
6081
+ }
6082
+ return;
6083
+ }
6084
+ if (message.type === "resync_required") {
6085
+ const reason = message.reason ?? "resync_required";
6086
+ handlers?.onEvent(optionalString({ immediate: true, reason }, "cursor", message.cursor));
6087
+ if (message.cursor !== void 0 || isInvalidCursorReason(reason)) {
6088
+ await persistBestEffort(resyncRealtimeState(currentRealtime, message.cursor, reason, driveIsoTimestamp(clock)));
6089
+ }
6090
+ return;
6091
+ }
6092
+ if (message.type === "error") {
6093
+ const error = message.message ?? message.code ?? "realtime error";
6094
+ if (isRealtimeAuthError(error) || isRealtimeAuthError(message.code)) {
6095
+ authFailed = true;
6096
+ connectionId += 1;
6097
+ clearReconnectTimer();
6098
+ handlers?.onAuthFailed(redactedRealtimeError(error));
6099
+ activeSocket?.close();
6100
+ activeSocket = void 0;
6101
+ return;
6102
+ }
6103
+ handlers?.onWarning?.(redactedRealtimeError(error));
6104
+ return;
6105
+ }
6106
+ handlers?.onWarning?.("unknown realtime message");
6107
+ }
6108
+ return {
6109
+ async start(nextHandlers) {
6110
+ handlers = nextHandlers;
6111
+ stopped = false;
6112
+ authFailed = false;
6113
+ connectNow();
6114
+ },
6115
+ async close() {
6116
+ stopped = true;
6117
+ clearReconnectTimer();
6118
+ activeSocket?.close();
6119
+ activeSocket = void 0;
6120
+ }
6121
+ };
6122
+ }
6123
+ function buildDriveRealtimeUrl(baseUrl, libraryId, realtime) {
6124
+ if (realtime.client_id.length === 0) {
6125
+ throw new Error("drive realtime client_id is required");
6126
+ }
6127
+ const url = new URL(baseUrl);
6128
+ url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
6129
+ url.pathname = `/drive/libraries/${encodeURIComponent(libraryId)}/realtime`;
6130
+ url.search = "";
6131
+ url.hash = "";
6132
+ if (realtime.last_cursor !== void 0) {
6133
+ url.searchParams.set("cursor", realtime.last_cursor);
6134
+ }
6135
+ url.searchParams.set("client_id", realtime.client_id);
6136
+ return url;
6137
+ }
6138
+ function parseDriveRealtimeMessage(raw) {
6139
+ let value;
6140
+ try {
6141
+ value = JSON.parse(raw);
6142
+ } catch {
6143
+ return { type: "unknown" };
6144
+ }
6145
+ if (!isRecord3(value)) {
6146
+ return { type: "unknown" };
6147
+ }
6148
+ const messageType = typeof value.type === "string" ? value.type : void 0;
6149
+ const cursor = typeof value.cursor === "string" ? value.cursor : void 0;
6150
+ if (messageType === "ready") {
6151
+ return optionalString({ type: "ready", replayed: typeof value.replayed === "number" ? value.replayed : 0 }, "cursor", cursor);
6152
+ }
6153
+ if (messageType === "library_changed") {
6154
+ return optionalString(optionalString({ type: "library_changed" }, "cursor", cursor), "path", value.path);
6155
+ }
6156
+ if (messageType === "resync_required") {
6157
+ return optionalString(optionalString({ type: "resync_required" }, "cursor", cursor), "reason", value.reason);
6158
+ }
6159
+ if (messageType === "error") {
6160
+ return optionalString(
6161
+ optionalString({ type: "error" }, "code", value.code),
6162
+ "message",
6163
+ typeof value.message === "string" ? redactedRealtimeError(value.message) : void 0
6164
+ );
6165
+ }
6166
+ return optionalString({ type: "unknown" }, "message_type", messageType);
6167
+ }
6168
+ function redactedRealtimeError(error) {
6169
+ const text = error instanceof Error ? error.message : String(error);
6170
+ const status = text.match(/\bHTTP\s+(401|403|429|5\d\d)\b/i);
6171
+ if (status?.[1] !== void 0) {
6172
+ return `HTTP ${status[1]}`;
6173
+ }
6174
+ if (/\bauth|authorization\b/i.test(text)) {
6175
+ return "auth failed";
6176
+ }
6177
+ if (/\bnetwork|fetch|close\b/i.test(text)) {
6178
+ return "network error";
6179
+ }
6180
+ return "realtime error";
6181
+ }
6182
+ function isRecord3(value) {
6183
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6184
+ }
6185
+ function resyncRealtimeState(realtime, cursor, reason, lastEventAt) {
6186
+ if (isInvalidCursorReason(reason)) {
6187
+ const { last_cursor: _lastCursor, ...next } = realtime;
6188
+ return { ...next, last_event_at: lastEventAt };
6189
+ }
6190
+ return optionalString({ ...realtime, last_event_at: lastEventAt }, "last_cursor", cursor);
6191
+ }
6192
+ function isInvalidCursorReason(reason) {
6193
+ return /\bcursor[_ -]?(invalid|expired|gone|missing|not[_ -]?found)\b|\binvalid[_ -]?cursor\b/i.test(reason);
6194
+ }
6195
+ function isRealtimeAuthError(error) {
6196
+ return /\b(401|403|auth|authorization|unauthorized|forbidden)\b/i.test(String(error));
6197
+ }
6198
+ function optionalString(target, key, value) {
6199
+ if (typeof value !== "string") {
6200
+ return target;
6201
+ }
6202
+ return { ...target, [key]: value };
6203
+ }
6204
+ function nativeWebSocketConnector(url, handlers, init) {
6205
+ const WebSocketWithInit = WebSocket;
6206
+ const ws = new WebSocketWithInit(url.toString(), init);
6207
+ let closed = false;
6208
+ let pendingError;
6209
+ const closeOnce = (error) => {
6210
+ if (closed) return;
6211
+ closed = true;
6212
+ handlers.close(error);
6213
+ };
6214
+ ws.addEventListener("open", () => handlers.open());
6215
+ ws.addEventListener("message", (event) => handlers.message(String(event.data)));
6216
+ ws.addEventListener("close", (event) => closeOnce(webSocketCloseError(event) ?? pendingError));
6217
+ ws.addEventListener("error", (event) => {
6218
+ pendingError = webSocketError(event);
6219
+ if (isRealtimeAuthError(pendingError)) {
6220
+ closeOnce(pendingError);
6221
+ }
6222
+ });
6223
+ return { close: () => ws.close() };
6224
+ }
6225
+ function webSocketCloseError(event) {
6226
+ if (event.code === 1e3) return void 0;
6227
+ if (event.code === 4001 || event.code === 4401) return new Error("HTTP 401");
6228
+ if (event.code === 4003 || event.code === 4403) return new Error("HTTP 403");
6229
+ if (event.reason && isRealtimeAuthError(event.reason)) return new Error(event.reason);
6230
+ if (event.reason) return new Error(event.reason);
6231
+ return new Error(`WebSocket close ${event.code}`);
6232
+ }
6233
+ function webSocketError(event) {
6234
+ if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
6235
+ const detail = event.error instanceof Error ? event.error.message : event.message;
6236
+ if (detail) return new Error(detail);
6237
+ }
6238
+ return new Error("network error");
6239
+ }
6240
+
6241
+ // src/handwritten/commands/drive/watch.ts
5240
6242
  async function runDriveWatch(root, options = {}) {
5241
6243
  const runSync = options.runSync ?? runDriveSyncOnce;
5242
6244
  const debounceMs = options.debounceMs ?? 500;
6245
+ const remoteDebounceMs = options.remoteDebounceMs ?? 2e3;
5243
6246
  const emit = options.onEvent ?? ((event) => render({ kind: "drive_watch", display: { shape: "object" } }, event));
5244
6247
  let debounceTimer;
6248
+ let debounceDeadlineMs;
5245
6249
  let retryTimer;
5246
6250
  let resolveRetryTimer;
5247
6251
  let running = false;
@@ -5269,6 +6273,7 @@ async function runDriveWatch(root, options = {}) {
5269
6273
  } catch (error) {
5270
6274
  if (isAuthError(error) || isFatalWatchError(error) || !isRetryableWatchError(error)) throw error;
5271
6275
  emit({ kind: "drive_watch_retry", delay_ms: backoffMs, error: errorMessage2(error) });
6276
+ if (stopped) return;
5272
6277
  await waitForManagedTimer(backoffMs);
5273
6278
  if (stopped) return;
5274
6279
  backoffMs = Math.min(backoffMs * 2, 6e4);
@@ -5283,6 +6288,27 @@ async function runDriveWatch(root, options = {}) {
5283
6288
  if (debounceTimer === void 0) return;
5284
6289
  clearTimeout(debounceTimer);
5285
6290
  debounceTimer = void 0;
6291
+ debounceDeadlineMs = void 0;
6292
+ }
6293
+ function scheduleSync(delayMs) {
6294
+ if (running) {
6295
+ rerunRequested = true;
6296
+ return;
6297
+ }
6298
+ if (delayMs <= 0) {
6299
+ clearDebounceTimer();
6300
+ requestSync().catch(stopWithError);
6301
+ return;
6302
+ }
6303
+ const deadlineMs = Date.now() + delayMs;
6304
+ if (debounceTimer !== void 0 && debounceDeadlineMs !== void 0 && debounceDeadlineMs <= deadlineMs) return;
6305
+ clearDebounceTimer();
6306
+ debounceDeadlineMs = deadlineMs;
6307
+ debounceTimer = setTimeout(() => {
6308
+ debounceTimer = void 0;
6309
+ debounceDeadlineMs = void 0;
6310
+ requestSync().catch(stopWithError);
6311
+ }, delayMs);
5286
6312
  }
5287
6313
  function clearRetryTimer() {
5288
6314
  if (retryTimer === void 0) return;
@@ -5304,26 +6330,61 @@ async function runDriveWatch(root, options = {}) {
5304
6330
  }
5305
6331
  function stopWithError(error) {
5306
6332
  stopError = error;
6333
+ stopped = true;
5307
6334
  clearDebounceTimer();
5308
6335
  clearRetryTimer();
5309
6336
  stopWatch?.();
5310
6337
  }
5311
- const state = await (options.readState ?? readDriveState)(root);
5312
- const source = options.source ?? createChokidarSource(root);
6338
+ let source;
6339
+ let realtimeSource;
5313
6340
  try {
6341
+ let state = await (options.readState ?? readDriveState)(root);
6342
+ realtimeSource = options.once ? void 0 : options.realtimeSource;
6343
+ if (!options.once && realtimeSource === void 0) {
6344
+ state = await ensureDriveRealtimeState(root);
6345
+ const { baseUrl, headers } = await loadRealtimeAuthHeaders({
6346
+ verifyPath: `/drive/libraries/${encodeURIComponent(state.library_id)}`
6347
+ });
6348
+ const realtime = state.realtime;
6349
+ if (realtime === void 0) {
6350
+ throw new Error("drive realtime state is required");
6351
+ }
6352
+ realtimeSource = createDriveRealtimeSource({
6353
+ baseUrl,
6354
+ headers,
6355
+ libraryId: state.library_id,
6356
+ realtime,
6357
+ writeRealtimeState: (next) => writeDriveRealtimeState(root, next)
6358
+ });
6359
+ }
6360
+ source = options.source ?? createChokidarSource(root);
5314
6361
  source.onChange((path) => {
5315
6362
  if (isDriveInternalPath(root, path)) return;
5316
- if (running) {
5317
- rerunRequested = true;
5318
- return;
5319
- }
5320
- clearDebounceTimer();
5321
- debounceTimer = setTimeout(() => {
5322
- debounceTimer = void 0;
5323
- requestSync().catch(stopWithError);
5324
- }, debounceMs);
6363
+ scheduleSync(debounceMs);
5325
6364
  });
5326
6365
  emit({ kind: "drive_watch_started", root, library_id: state.library_id });
6366
+ realtimeSource?.start({
6367
+ onConnected() {
6368
+ emit({ kind: "drive_realtime_connected", library_id: state.library_id });
6369
+ },
6370
+ onEvent(event) {
6371
+ emit(realtimeEvent(event));
6372
+ if (event.immediate) {
6373
+ scheduleSync(0);
6374
+ return;
6375
+ }
6376
+ scheduleSync(event.debounce_ms ?? remoteDebounceMs);
6377
+ },
6378
+ onReconnect(delayMs, error) {
6379
+ emit({ kind: "drive_realtime_reconnecting", delay_ms: delayMs, error });
6380
+ },
6381
+ onAuthFailed(error) {
6382
+ emit({ kind: "drive_realtime_auth_failed", error: error ?? "auth failed" });
6383
+ },
6384
+ onWarning(warning) {
6385
+ emit({ kind: "drive_realtime_warning", warning });
6386
+ }
6387
+ }).catch(stopWithError);
5327
6388
  await requestSync();
5328
6389
  if (options.once) return;
5329
6390
  if (stopError !== void 0) throw stopError;
@@ -5337,11 +6398,11 @@ async function runDriveWatch(root, options = {}) {
5337
6398
  clearDebounceTimer();
5338
6399
  clearRetryTimer();
5339
6400
  cleanupSignalListeners();
5340
- await source.close();
6401
+ await Promise.all([source?.close(), realtimeSource?.close()]);
5341
6402
  }
5342
6403
  }
5343
6404
  function driveWatchCommand(options = {}) {
5344
- return new Command65("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").action(async (path) => {
6405
+ return new Command72("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").action(async (path) => {
5345
6406
  await runDriveWatch(resolve4(path), options);
5346
6407
  });
5347
6408
  }
@@ -5379,6 +6440,15 @@ function isRetryableWatchError(error) {
5379
6440
  function errorMessage2(error) {
5380
6441
  return error instanceof Error ? error.message : String(error);
5381
6442
  }
6443
+ function realtimeEvent(event) {
6444
+ return {
6445
+ kind: "drive_realtime_event",
6446
+ message: "remote update received; syncing",
6447
+ ...event.path === void 0 ? {} : { path: event.path },
6448
+ ...event.reason === void 0 ? {} : { reason: event.reason },
6449
+ ...event.cursor === void 0 ? {} : { cursor: event.cursor }
6450
+ };
6451
+ }
5382
6452
  function waitForStopSignal(onRegistered) {
5383
6453
  return new Promise((resolveStop) => {
5384
6454
  const stop = () => resolveStop();
@@ -5396,7 +6466,7 @@ function waitForStopSignal(onRegistered) {
5396
6466
  function mountDriveCommands(program) {
5397
6467
  let drive = program.commands.find((c) => c.name() === "drive");
5398
6468
  if (!drive) {
5399
- drive = new Command66("drive").description("Drive commands");
6469
+ drive = new Command73("drive").description("Drive commands");
5400
6470
  program.addCommand(drive);
5401
6471
  }
5402
6472
  if (!drive.commands.some((c) => c.name() === "bind")) {
@@ -5422,7 +6492,7 @@ function isCliEntrypoint(argv = process.argv, metaUrl = import.meta.url) {
5422
6492
  }
5423
6493
  }
5424
6494
  function buildProgram() {
5425
- const program = new Command66().name("wspc").description("Official CLI for wspc.ai").version(`wspc ${VERSION} (spec ${SPEC_SHA}, fetched ${SPEC_FETCHED_AT})`).option("--json", "Output raw JSON (machine-readable)").option("--account <email>", "Run as a specific account (overrides the active account)").hook("preAction", (_thisCommand, actionCommand) => {
6495
+ const program = new Command73().name("wspc").description("Official CLI for wspc.ai").version(`wspc ${VERSION} (spec ${SPEC_SHA}, fetched ${SPEC_FETCHED_AT})`).option("--json", "Output raw JSON (machine-readable)").option("--account <email>", "Run as a specific account (overrides the active account)").hook("preAction", (_thisCommand, actionCommand) => {
5426
6496
  const globals = actionCommand.optsWithGlobals();
5427
6497
  if (globals.json) process.env.WSPC_OUTPUT = "json";
5428
6498
  if (globals.account) process.env.WSPC_ACCOUNT = String(globals.account);