@narumitw/pi-sync 0.50.3 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +89 -101
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +197 -205
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  176. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  177. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  178. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  179. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  180. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  181. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  182. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  183. package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  186. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  187. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  188. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  189. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  190. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  191. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  192. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  193. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  194. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  195. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  196. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  197. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  198. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  199. package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
  200. package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
  206. package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -3,15 +3,15 @@ import { isDeniedPath, toPosix } from "../paths.js";
3
3
  import type { Snapshot, SnapshotSelection } from "../snapshot/snapshot-types.js";
4
4
 
5
5
  export const BUILT_IN_SYNC_ROOTS = [
6
- "settings.json",
7
- "keybindings.json",
8
- "models.json",
9
- "AGENTS.md",
10
- "APPEND_SYSTEM.md",
11
- "skills",
12
- "prompts",
13
- "themes",
14
- "extensions",
6
+ "settings.json",
7
+ "keybindings.json",
8
+ "models.json",
9
+ "AGENTS.md",
10
+ "APPEND_SYSTEM.md",
11
+ "skills",
12
+ "prompts",
13
+ "themes",
14
+ "extensions",
15
15
  ] as const;
16
16
 
17
17
  export const DEFAULT_SYNC_INCLUDE = [...BUILT_IN_SYNC_ROOTS] as const;
@@ -22,417 +22,387 @@ const MAX_SYNC_INCLUDE_PATH_BYTES = 4_096;
22
22
  const MAX_SYNC_INCLUDE_TOTAL_BYTES = 256 * 1_024;
23
23
 
24
24
  interface IncludePathNode {
25
- selected?: string;
26
- children: Map<string, IncludePathNode>;
25
+ selected?: string;
26
+ children: Map<string, IncludePathNode>;
27
27
  }
28
28
 
29
29
  const BUILT_IN_BY_LOWER = new Map<string, BuiltInSyncFile>(
30
- BUILT_IN_SYNC_ROOTS.map((fileName) => [fileName.toLowerCase(), fileName]),
30
+ BUILT_IN_SYNC_ROOTS.map((fileName) => [fileName.toLowerCase(), fileName]),
31
31
  );
32
32
  const TOP_LEVEL_FILE_PATHS = new Map<string, string>(
33
- BUILT_IN_SYNC_ROOTS.filter((fileName) => fileName.includes(".")).map((fileName) => [
34
- fileName.toLowerCase(),
35
- fileName,
36
- ]),
37
- );
38
- const TOP_LEVEL_DIRS = new Set<string>(
39
- BUILT_IN_SYNC_ROOTS.filter((fileName) => !fileName.includes(".")),
33
+ BUILT_IN_SYNC_ROOTS.filter((fileName) => fileName.includes(".")).map((fileName) => [
34
+ fileName.toLowerCase(),
35
+ fileName,
36
+ ]),
40
37
  );
38
+ const TOP_LEVEL_DIRS = new Set<string>(BUILT_IN_SYNC_ROOTS.filter((fileName) => !fileName.includes(".")));
41
39
  const RESERVED_TOP_LEVEL_NAMES = new Set<string>([...BUILT_IN_BY_LOWER.keys(), "sessions"]);
42
40
 
43
41
  export function normalizeSyncInclude(value: unknown): string[] {
44
- if (!Array.isArray(value)) {
45
- throw new Error("Invalid pi-sync settings: sync.include must be an array.");
46
- }
47
- if (value.length > MAX_SYNC_INCLUDE_ITEMS) {
48
- throw new Error(
49
- `Invalid pi-sync settings: sync.include has too many items; limit: ${MAX_SYNC_INCLUDE_ITEMS}.`,
50
- );
51
- }
52
- const result: string[] = [];
53
- const seen = new Set<string>();
54
- const pathRoot: IncludePathNode = { children: new Map() };
55
- let totalBytes = 0;
56
- for (const item of value) {
57
- if (typeof item !== "string") {
58
- throw new Error("Invalid pi-sync settings: sync.include items must be strings.");
59
- }
60
- const itemBytes = Buffer.byteLength(item, "utf8");
61
- if (itemBytes > MAX_SYNC_INCLUDE_PATH_BYTES) {
62
- throw new Error(
63
- `Invalid pi-sync settings: sync.include item is too long; limit: ${MAX_SYNC_INCLUDE_PATH_BYTES} bytes.`,
64
- );
65
- }
66
- totalBytes += itemBytes;
67
- if (totalBytes > MAX_SYNC_INCLUDE_TOTAL_BYTES) {
68
- throw new Error(
69
- `Invalid pi-sync settings: sync.include is too large; limit: ${MAX_SYNC_INCLUDE_TOTAL_BYTES} bytes.`,
70
- );
71
- }
72
- const trimmed = item.trim();
73
- const builtIn = BUILT_IN_BY_LOWER.get(trimmed.toLowerCase());
74
- const normalized = builtIn ?? (trimmed.toLowerCase() === "sessions" ? "sessions" : trimmed);
75
- if (!builtIn && normalized !== "sessions") validateAgentRelativeInclude(normalized);
76
- const identity = normalized.toLowerCase();
77
- if (seen.has(identity)) {
78
- throw new Error(`Invalid pi-sync settings: duplicate sync.include item: ${item}`);
79
- }
80
- addIncludePath(pathRoot, identity, item);
81
- seen.add(identity);
82
- result.push(normalized);
83
- }
84
- return result;
42
+ if (!Array.isArray(value)) {
43
+ throw new Error("Invalid pi-sync settings: sync.include must be an array.");
44
+ }
45
+ if (value.length > MAX_SYNC_INCLUDE_ITEMS) {
46
+ throw new Error(`Invalid pi-sync settings: sync.include has too many items; limit: ${MAX_SYNC_INCLUDE_ITEMS}.`);
47
+ }
48
+ const result: string[] = [];
49
+ const seen = new Set<string>();
50
+ const pathRoot: IncludePathNode = { children: new Map() };
51
+ let totalBytes = 0;
52
+ for (const item of value) {
53
+ if (typeof item !== "string") {
54
+ throw new Error("Invalid pi-sync settings: sync.include items must be strings.");
55
+ }
56
+ const itemBytes = Buffer.byteLength(item, "utf8");
57
+ if (itemBytes > MAX_SYNC_INCLUDE_PATH_BYTES) {
58
+ throw new Error(
59
+ `Invalid pi-sync settings: sync.include item is too long; limit: ${MAX_SYNC_INCLUDE_PATH_BYTES} bytes.`,
60
+ );
61
+ }
62
+ totalBytes += itemBytes;
63
+ if (totalBytes > MAX_SYNC_INCLUDE_TOTAL_BYTES) {
64
+ throw new Error(
65
+ `Invalid pi-sync settings: sync.include is too large; limit: ${MAX_SYNC_INCLUDE_TOTAL_BYTES} bytes.`,
66
+ );
67
+ }
68
+ const trimmed = item.trim();
69
+ const builtIn = BUILT_IN_BY_LOWER.get(trimmed.toLowerCase());
70
+ const normalized = builtIn ?? (trimmed.toLowerCase() === "sessions" ? "sessions" : trimmed);
71
+ if (!builtIn && normalized !== "sessions") validateAgentRelativeInclude(normalized);
72
+ const identity = normalized.toLowerCase();
73
+ if (seen.has(identity)) {
74
+ throw new Error(`Invalid pi-sync settings: duplicate sync.include item: ${item}`);
75
+ }
76
+ addIncludePath(pathRoot, identity, item);
77
+ seen.add(identity);
78
+ result.push(normalized);
79
+ }
80
+ return result;
85
81
  }
86
82
 
87
83
  function addIncludePath(root: IncludePathNode, identity: string, source: string) {
88
- let node = root;
89
- for (const segment of identity.split("/")) {
90
- if (node.selected !== undefined) throwOverlappingInclude(source);
91
- let child = node.children.get(segment);
92
- if (!child) {
93
- child = { children: new Map() };
94
- node.children.set(segment, child);
95
- }
96
- node = child;
97
- }
98
- if (node.children.size > 0) throwOverlappingInclude(source);
99
- node.selected = identity;
84
+ let node = root;
85
+ for (const segment of identity.split("/")) {
86
+ if (node.selected !== undefined) throwOverlappingInclude(source);
87
+ let child = node.children.get(segment);
88
+ if (!child) {
89
+ child = { children: new Map() };
90
+ node.children.set(segment, child);
91
+ }
92
+ node = child;
93
+ }
94
+ if (node.children.size > 0) throwOverlappingInclude(source);
95
+ node.selected = identity;
100
96
  }
101
97
 
102
98
  function throwOverlappingInclude(item: string): never {
103
- throw new Error(
104
- `Invalid pi-sync settings: overlapping sync.include items are ambiguous: ${item}`,
105
- );
99
+ throw new Error(`Invalid pi-sync settings: overlapping sync.include items are ambiguous: ${item}`);
106
100
  }
107
101
 
108
102
  function validateAgentRelativeInclude(value: string) {
109
- const normalized = toPosix(value);
110
- const topLevel = normalized.split("/")[0]?.toLowerCase();
111
- if (
112
- !normalized ||
113
- normalized === "." ||
114
- normalized === ".." ||
115
- normalized.startsWith("../") ||
116
- path.posix.isAbsolute(normalized) ||
117
- normalized.includes("\\") ||
118
- path.posix.normalize(normalized) !== normalized ||
119
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Include paths cannot contain controls.
120
- /[\u0000-\u001f\u007f-\u009f]/u.test(normalized)
121
- ) {
122
- throw new Error(
123
- `Invalid pi-sync settings: sync.include item must be a safe agent-relative path: ${value}`,
124
- );
125
- }
126
- if (isDeniedPath(normalized)) {
127
- throw new Error(`Invalid pi-sync settings: ${value} cannot be synced.`);
128
- }
129
- if (topLevel && RESERVED_TOP_LEVEL_NAMES.has(topLevel)) {
130
- throw new Error(
131
- `Invalid pi-sync settings: use the canonical ${topLevel} root instead of a nested sync.include path.`,
132
- );
133
- }
103
+ const normalized = toPosix(value);
104
+ const topLevel = normalized.split("/")[0]?.toLowerCase();
105
+ if (
106
+ !normalized ||
107
+ normalized === "." ||
108
+ normalized === ".." ||
109
+ normalized.startsWith("../") ||
110
+ path.posix.isAbsolute(normalized) ||
111
+ normalized.includes("\\") ||
112
+ path.posix.normalize(normalized) !== normalized ||
113
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Include paths cannot contain controls.
114
+ /[\u0000-\u001f\u007f-\u009f]/u.test(normalized)
115
+ ) {
116
+ throw new Error(`Invalid pi-sync settings: sync.include item must be a safe agent-relative path: ${value}`);
117
+ }
118
+ if (isDeniedPath(normalized)) {
119
+ throw new Error(`Invalid pi-sync settings: ${value} cannot be synced.`);
120
+ }
121
+ if (topLevel && RESERVED_TOP_LEVEL_NAMES.has(topLevel)) {
122
+ throw new Error(
123
+ `Invalid pi-sync settings: use the canonical ${topLevel} root instead of a nested sync.include path.`,
124
+ );
125
+ }
134
126
  }
135
127
 
136
128
  export function portableSnapshotSelection(value: unknown): SnapshotSelection {
137
- const selection = value as Partial<SnapshotSelection> | null;
138
- if (
139
- !selection ||
140
- typeof selection !== "object" ||
141
- Array.isArray(selection) ||
142
- selection.version !== SNAPSHOT_SELECTION_VERSION ||
143
- !Object.hasOwn(selection, "include") ||
144
- Object.keys(selection).some((key) => key !== "version" && key !== "include")
145
- ) {
146
- throw new Error("Invalid snapshot selection policy.");
147
- }
148
- return {
149
- version: SNAPSHOT_SELECTION_VERSION,
150
- include: normalizeSyncInclude(selection.include),
151
- };
129
+ const selection = value as Partial<SnapshotSelection> | null;
130
+ if (
131
+ !selection ||
132
+ typeof selection !== "object" ||
133
+ Array.isArray(selection) ||
134
+ selection.version !== SNAPSHOT_SELECTION_VERSION ||
135
+ !Object.hasOwn(selection, "include") ||
136
+ Object.keys(selection).some((key) => key !== "version" && key !== "include")
137
+ ) {
138
+ throw new Error("Invalid snapshot selection policy.");
139
+ }
140
+ return {
141
+ version: SNAPSHOT_SELECTION_VERSION,
142
+ include: normalizeSyncInclude(selection.include),
143
+ };
152
144
  }
153
145
 
154
146
  export function snapshotSelectionInclude(snapshot: Pick<Snapshot, "selection">) {
155
- return snapshot.selection === undefined
156
- ? undefined
157
- : portableSnapshotSelection(snapshot.selection).include;
147
+ return snapshot.selection === undefined ? undefined : portableSnapshotSelection(snapshot.selection).include;
158
148
  }
159
149
 
160
150
  export function selectionForSnapshot(include: unknown): SnapshotSelection {
161
- return { version: SNAPSHOT_SELECTION_VERSION, include: normalizeSyncInclude(include) };
151
+ return { version: SNAPSHOT_SELECTION_VERSION, include: normalizeSyncInclude(include) };
162
152
  }
163
153
 
164
154
  export function sameSyncInclude(left: unknown, right: unknown) {
165
- const normalizedLeft = normalizeSyncInclude(left);
166
- const normalizedRight = normalizeSyncInclude(right);
167
- return (
168
- normalizedLeft.length === normalizedRight.length &&
169
- normalizedLeft.every((item, index) => item === normalizedRight[index])
170
- );
155
+ const normalizedLeft = normalizeSyncInclude(left);
156
+ const normalizedRight = normalizeSyncInclude(right);
157
+ return (
158
+ normalizedLeft.length === normalizedRight.length &&
159
+ normalizedLeft.every((item, index) => item === normalizedRight[index])
160
+ );
171
161
  }
172
162
 
173
163
  export function compareSyncInclude(local: unknown, remote: unknown) {
174
- const localInclude = normalizeSyncInclude(local);
175
- const remoteInclude = normalizeSyncInclude(remote);
176
- const localSet = new Set(localInclude);
177
- const remoteSet = new Set(remoteInclude);
178
- return {
179
- same: sameSyncInclude(localInclude, remoteInclude),
180
- remoteOnly: remoteInclude.filter((item) => !localSet.has(item)),
181
- localOnly: localInclude.filter((item) => !remoteSet.has(item)),
182
- };
164
+ const localInclude = normalizeSyncInclude(local);
165
+ const remoteInclude = normalizeSyncInclude(remote);
166
+ const localSet = new Set(localInclude);
167
+ const remoteSet = new Set(remoteInclude);
168
+ return {
169
+ same: sameSyncInclude(localInclude, remoteInclude),
170
+ remoteOnly: remoteInclude.filter((item) => !localSet.has(item)),
171
+ localOnly: localInclude.filter((item) => !remoteSet.has(item)),
172
+ };
183
173
  }
184
174
 
185
175
  export type RemoteSelectionState =
186
- | { kind: "legacy"; discovered: string[] }
187
- | { kind: "same"; include: string[] }
188
- | {
189
- kind: "different";
190
- include: string[];
191
- remoteOnly: string[];
192
- localOnly: string[];
193
- };
176
+ | { kind: "legacy"; discovered: string[] }
177
+ | { kind: "same"; include: string[] }
178
+ | {
179
+ kind: "different";
180
+ include: string[];
181
+ remoteOnly: string[];
182
+ localOnly: string[];
183
+ };
194
184
 
195
185
  export function inspectRemoteSelection(
196
- localInclude: unknown,
197
- snapshot: Pick<Snapshot, "selection" | "files">,
186
+ localInclude: unknown,
187
+ snapshot: Pick<Snapshot, "selection" | "files">,
198
188
  ): RemoteSelectionState {
199
- const remoteInclude = snapshotSelectionInclude(snapshot);
200
- if (!remoteInclude) {
201
- return { kind: "legacy", discovered: discoverLegacySnapshotInclude(snapshot) };
202
- }
203
- const comparison = compareSyncInclude(localInclude, remoteInclude);
204
- return comparison.same
205
- ? { kind: "same", include: remoteInclude }
206
- : { kind: "different", include: remoteInclude, ...comparison };
189
+ const remoteInclude = snapshotSelectionInclude(snapshot);
190
+ if (!remoteInclude) {
191
+ return { kind: "legacy", discovered: discoverLegacySnapshotInclude(snapshot) };
192
+ }
193
+ const comparison = compareSyncInclude(localInclude, remoteInclude);
194
+ return comparison.same
195
+ ? { kind: "same", include: remoteInclude }
196
+ : { kind: "different", include: remoteInclude, ...comparison };
207
197
  }
208
198
 
209
199
  export interface RemoteSelectionDecision {
210
- setupName: string;
211
- configIdentity: string;
212
- localInclude: readonly string[];
213
- remoteInclude: readonly string[];
200
+ setupName: string;
201
+ configIdentity: string;
202
+ localInclude: readonly string[];
203
+ remoteInclude: readonly string[];
214
204
  }
215
205
 
216
206
  export class RemoteSelectionMismatchError extends Error {
217
- readonly decision: RemoteSelectionDecision;
218
- readonly setupName: string;
219
- readonly localInclude: string[];
220
- readonly remoteInclude: string[];
221
-
222
- constructor(
223
- setupName: string,
224
- localInclude: unknown,
225
- remoteInclude: unknown,
226
- configIdentity = JSON.stringify([setupName, normalizeSyncInclude(localInclude)]),
227
- ) {
228
- const local = normalizeSyncInclude(localInclude);
229
- const remote = normalizeSyncInclude(remoteInclude);
230
- super(formatRemoteSelectionMismatch(setupName, local, remote));
231
- this.name = "RemoteSelectionMismatchError";
232
- this.setupName = setupName;
233
- this.localInclude = local;
234
- this.remoteInclude = remote;
235
- this.decision = {
236
- setupName,
237
- configIdentity,
238
- localInclude: [...local],
239
- remoteInclude: [...remote],
240
- };
241
- }
207
+ readonly decision: RemoteSelectionDecision;
208
+ readonly setupName: string;
209
+ readonly localInclude: string[];
210
+ readonly remoteInclude: string[];
211
+
212
+ constructor(
213
+ setupName: string,
214
+ localInclude: unknown,
215
+ remoteInclude: unknown,
216
+ configIdentity = JSON.stringify([setupName, normalizeSyncInclude(localInclude)]),
217
+ ) {
218
+ const local = normalizeSyncInclude(localInclude);
219
+ const remote = normalizeSyncInclude(remoteInclude);
220
+ super(formatRemoteSelectionMismatch(setupName, local, remote));
221
+ this.name = "RemoteSelectionMismatchError";
222
+ this.setupName = setupName;
223
+ this.localInclude = local;
224
+ this.remoteInclude = remote;
225
+ this.decision = {
226
+ setupName,
227
+ configIdentity,
228
+ localInclude: [...local],
229
+ remoteInclude: [...remote],
230
+ };
231
+ }
242
232
  }
243
233
 
244
234
  export function remoteSelectionMismatch(
245
- config: { setupName: string; include: unknown },
246
- remoteInclude: unknown,
247
- configIdentity?: string,
235
+ config: { setupName: string; include: unknown },
236
+ remoteInclude: unknown,
237
+ configIdentity?: string,
248
238
  ) {
249
- return new RemoteSelectionMismatchError(
250
- config.setupName,
251
- config.include,
252
- remoteInclude,
253
- configIdentity,
254
- );
239
+ return new RemoteSelectionMismatchError(config.setupName, config.include, remoteInclude, configIdentity);
255
240
  }
256
241
 
257
242
  export function formatRemoteSelectionMismatch(
258
- setupName: string,
259
- localInclude: readonly string[],
260
- remoteInclude: readonly string[],
243
+ setupName: string,
244
+ localInclude: readonly string[],
245
+ remoteInclude: readonly string[],
261
246
  ) {
262
- const comparison = compareSyncInclude(localInclude, remoteInclude);
263
- const lines = [
264
- `Synced content differs for sync setup “${stripTerminalControls(setupName)}”.`,
265
- `Remote-only: ${comparison.remoteOnly.join(", ") || "none"}`,
266
- `This-device-only: ${comparison.localOnly.join(", ") || "none"}`,
267
- ];
268
- if (comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0) {
269
- lines.push(
270
- "Only ordering differs.",
271
- `Remote order: ${remoteInclude.join(", ") || "none"}`,
272
- `This device order: ${localInclude.join(", ") || "none"}`,
273
- );
274
- }
275
- lines.push("Run /sync in TUI to review both content lists and choose what happens next.");
276
- return lines.join("\n");
247
+ const comparison = compareSyncInclude(localInclude, remoteInclude);
248
+ const lines = [
249
+ `Synced content differs for sync setup “${stripTerminalControls(setupName)}”.`,
250
+ `Remote-only: ${comparison.remoteOnly.join(", ") || "none"}`,
251
+ `This-device-only: ${comparison.localOnly.join(", ") || "none"}`,
252
+ ];
253
+ if (comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0) {
254
+ lines.push(
255
+ "Only ordering differs.",
256
+ `Remote order: ${remoteInclude.join(", ") || "none"}`,
257
+ `This device order: ${localInclude.join(", ") || "none"}`,
258
+ );
259
+ }
260
+ lines.push("Run /sync in TUI to review both content lists and choose what happens next.");
261
+ return lines.join("\n");
277
262
  }
278
263
 
279
264
  function stripTerminalControls(value: string) {
280
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
281
- return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
265
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
266
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
282
267
  }
283
268
 
284
269
  export function discoverLegacySnapshotInclude(snapshot: Pick<Snapshot, "files">) {
285
- const builtIns = new Set<BuiltInSyncFile>();
286
- const custom = new Set<string>();
287
- let sessions = false;
288
- for (const file of snapshot.files) {
289
- const normalized = toPosix(file.path);
290
- if (
291
- !normalized ||
292
- file.path.includes("\\") ||
293
- normalized.length > 4096 ||
294
- normalized.startsWith("../") ||
295
- path.posix.isAbsolute(normalized) ||
296
- path.posix.normalize(normalized) !== normalized ||
297
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Ignore unsafe legacy paths.
298
- /[\u0000-\u001f\u007f-\u009f]/u.test(normalized) ||
299
- isDeniedPath(normalized)
300
- ) {
301
- continue;
302
- }
303
- const [topLevel, ...rest] = normalized.split("/");
304
- if (!topLevel) continue;
305
- if (topLevel === "sessions" && rest.length > 0) {
306
- sessions = true;
307
- continue;
308
- }
309
- const builtIn = BUILT_IN_BY_LOWER.get(topLevel.toLowerCase());
310
- if (
311
- builtIn &&
312
- ((TOP_LEVEL_DIRS.has(builtIn) && rest.length > 0) ||
313
- (!TOP_LEVEL_DIRS.has(builtIn) && rest.length === 0))
314
- ) {
315
- builtIns.add(builtIn);
316
- continue;
317
- }
318
- if (isSafeCustomIncludePath(topLevel)) custom.add(topLevel);
319
- }
320
- return [
321
- ...BUILT_IN_SYNC_ROOTS.filter((item) => builtIns.has(item)),
322
- ...[...custom].sort((left, right) => left.localeCompare(right)),
323
- ...(sessions ? ["sessions"] : []),
324
- ];
270
+ const builtIns = new Set<BuiltInSyncFile>();
271
+ const custom = new Set<string>();
272
+ let sessions = false;
273
+ for (const file of snapshot.files) {
274
+ const normalized = toPosix(file.path);
275
+ if (
276
+ !normalized ||
277
+ file.path.includes("\\") ||
278
+ normalized.length > 4096 ||
279
+ normalized.startsWith("../") ||
280
+ path.posix.isAbsolute(normalized) ||
281
+ path.posix.normalize(normalized) !== normalized ||
282
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Ignore unsafe legacy paths.
283
+ /[\u0000-\u001f\u007f-\u009f]/u.test(normalized) ||
284
+ isDeniedPath(normalized)
285
+ ) {
286
+ continue;
287
+ }
288
+ const [topLevel, ...rest] = normalized.split("/");
289
+ if (!topLevel) continue;
290
+ if (topLevel === "sessions" && rest.length > 0) {
291
+ sessions = true;
292
+ continue;
293
+ }
294
+ const builtIn = BUILT_IN_BY_LOWER.get(topLevel.toLowerCase());
295
+ if (
296
+ builtIn &&
297
+ ((TOP_LEVEL_DIRS.has(builtIn) && rest.length > 0) || (!TOP_LEVEL_DIRS.has(builtIn) && rest.length === 0))
298
+ ) {
299
+ builtIns.add(builtIn);
300
+ continue;
301
+ }
302
+ if (isSafeCustomIncludePath(topLevel)) custom.add(topLevel);
303
+ }
304
+ return [
305
+ ...BUILT_IN_SYNC_ROOTS.filter((item) => builtIns.has(item)),
306
+ ...[...custom].sort((left, right) => left.localeCompare(right)),
307
+ ...(sessions ? ["sessions"] : []),
308
+ ];
325
309
  }
326
310
 
327
311
  export function syncIncludeSelection(value: unknown) {
328
- const include = normalizeSyncInclude(value);
329
- const builtIns = include.filter((item): item is BuiltInSyncFile =>
330
- BUILT_IN_BY_LOWER.has(item.toLowerCase()),
331
- );
332
- const custom = include.filter(
333
- (item) => item !== "sessions" && !BUILT_IN_BY_LOWER.has(item.toLowerCase()),
334
- );
335
- return { include, builtIns, custom, sessions: include.includes("sessions") };
312
+ const include = normalizeSyncInclude(value);
313
+ const builtIns = include.filter((item): item is BuiltInSyncFile => BUILT_IN_BY_LOWER.has(item.toLowerCase()));
314
+ const custom = include.filter((item) => item !== "sessions" && !BUILT_IN_BY_LOWER.has(item.toLowerCase()));
315
+ return { include, builtIns, custom, sessions: include.includes("sessions") };
336
316
  }
337
317
 
338
318
  export function customIncludePathsByLower(value: unknown) {
339
- return new Map(
340
- syncIncludeSelection(value).custom.map((relativePath) => [
341
- relativePath.toLowerCase(),
342
- relativePath,
343
- ]),
344
- );
319
+ return new Map(syncIncludeSelection(value).custom.map((relativePath) => [relativePath.toLowerCase(), relativePath]));
345
320
  }
346
321
 
347
322
  export interface SyncSelectionConfig {
348
- include?: unknown;
349
- syncFiles?: unknown;
350
- syncSessions?: boolean;
351
- extraFiles?: unknown;
323
+ include?: unknown;
324
+ syncFiles?: unknown;
325
+ syncSessions?: boolean;
326
+ extraFiles?: unknown;
352
327
  }
353
328
 
354
329
  export function includeFromSelectionConfig(config: SyncSelectionConfig) {
355
- if (config.include !== undefined) return normalizeSyncInclude(config.include);
356
- return [
357
- ...normalizeSyncFiles(config.syncFiles),
358
- ...normalizeExtraFiles(config.extraFiles),
359
- ...(config.syncSessions ? ["sessions"] : []),
360
- ];
330
+ if (config.include !== undefined) return normalizeSyncInclude(config.include);
331
+ return [
332
+ ...normalizeSyncFiles(config.syncFiles),
333
+ ...normalizeExtraFiles(config.extraFiles),
334
+ ...(config.syncSessions ? ["sessions"] : []),
335
+ ];
361
336
  }
362
337
 
363
338
  export function isConfiguredSnapshotPath(
364
- relativePath: string,
365
- config: SyncSelectionConfig,
366
- _legacyExtraFiles?: Set<string>,
339
+ relativePath: string,
340
+ config: SyncSelectionConfig,
341
+ _legacyExtraFiles?: Set<string>,
367
342
  ) {
368
- const normalized = toPosix(relativePath);
369
- const selection = syncIncludeSelection(includeFromSelectionConfig(config));
370
- if (normalized.startsWith("sessions/")) return selection.sessions;
371
- const lower = normalized.toLowerCase();
372
- if (!normalized.includes("/")) {
373
- const builtIn = BUILT_IN_BY_LOWER.get(lower);
374
- if (builtIn) return selection.builtIns.includes(builtIn) && !TOP_LEVEL_DIRS.has(builtIn);
375
- }
376
- const topLevel = normalized.slice(0, normalized.indexOf("/"));
377
- if (selection.builtIns.includes(topLevel as BuiltInSyncFile) && TOP_LEVEL_DIRS.has(topLevel)) {
378
- return true;
379
- }
380
- return selection.custom.some((candidate) => {
381
- const candidateLower = candidate.toLowerCase();
382
- return lower === candidateLower || lower.startsWith(`${candidateLower}/`);
383
- });
343
+ const normalized = toPosix(relativePath);
344
+ const selection = syncIncludeSelection(includeFromSelectionConfig(config));
345
+ if (normalized.startsWith("sessions/")) return selection.sessions;
346
+ const lower = normalized.toLowerCase();
347
+ if (!normalized.includes("/")) {
348
+ const builtIn = BUILT_IN_BY_LOWER.get(lower);
349
+ if (builtIn) return selection.builtIns.includes(builtIn) && !TOP_LEVEL_DIRS.has(builtIn);
350
+ }
351
+ const topLevel = normalized.slice(0, normalized.indexOf("/"));
352
+ if (selection.builtIns.includes(topLevel as BuiltInSyncFile) && TOP_LEVEL_DIRS.has(topLevel)) {
353
+ return true;
354
+ }
355
+ return selection.custom.some((candidate) => {
356
+ const candidateLower = candidate.toLowerCase();
357
+ return lower === candidateLower || lower.startsWith(`${candidateLower}/`);
358
+ });
384
359
  }
385
360
 
386
- export function canonicalSnapshotPathForConfig(
387
- relativePath: string,
388
- includePaths: Map<string, string>,
389
- ) {
390
- const normalized = toPosix(relativePath);
391
- const lower = normalized.toLowerCase();
392
- return TOP_LEVEL_FILE_PATHS.get(lower) ?? includePaths.get(lower) ?? normalized;
361
+ export function canonicalSnapshotPathForConfig(relativePath: string, includePaths: Map<string, string>) {
362
+ const normalized = toPosix(relativePath);
363
+ const lower = normalized.toLowerCase();
364
+ return TOP_LEVEL_FILE_PATHS.get(lower) ?? includePaths.get(lower) ?? normalized;
393
365
  }
394
366
 
395
367
  export function isPreservableUnmanagedSnapshotPath(relativePath: string) {
396
- const normalized = toPosix(relativePath);
397
- if (!normalized || isDeniedPath(normalized)) return false;
398
- if (normalized.startsWith("sessions/")) return normalized.endsWith(".jsonl");
399
- if (!normalized.includes("/")) {
400
- const lower = normalized.toLowerCase();
401
- return TOP_LEVEL_FILE_PATHS.has(lower) || !RESERVED_TOP_LEVEL_NAMES.has(lower);
402
- }
403
- return true;
368
+ const normalized = toPosix(relativePath);
369
+ if (!normalized || isDeniedPath(normalized)) return false;
370
+ if (normalized.startsWith("sessions/")) return normalized.endsWith(".jsonl");
371
+ if (!normalized.includes("/")) {
372
+ const lower = normalized.toLowerCase();
373
+ return TOP_LEVEL_FILE_PATHS.has(lower) || !RESERVED_TOP_LEVEL_NAMES.has(lower);
374
+ }
375
+ return true;
404
376
  }
405
377
 
406
378
  export function isSafeCustomIncludePath(relativePath: string) {
407
- try {
408
- validateAgentRelativeInclude(relativePath);
409
- return true;
410
- } catch {
411
- return false;
412
- }
379
+ try {
380
+ validateAgentRelativeInclude(relativePath);
381
+ return true;
382
+ } catch {
383
+ return false;
384
+ }
413
385
  }
414
386
 
415
387
  export function isBuiltInTopLevelFile(fileName: string) {
416
- return TOP_LEVEL_FILE_PATHS.has(path.posix.basename(fileName).toLowerCase());
388
+ return TOP_LEVEL_FILE_PATHS.has(path.posix.basename(fileName).toLowerCase());
417
389
  }
418
390
 
419
391
  /** Compatibility projection for backend-neutral output while callers move to sync.include. */
420
392
  export function normalizeSyncFiles(value: unknown): BuiltInSyncFile[] {
421
- if (value === undefined) return [...DEFAULT_SYNC_INCLUDE];
422
- if (Array.isArray(value)) {
423
- return normalizeSyncInclude(value).filter((item): item is BuiltInSyncFile =>
424
- BUILT_IN_BY_LOWER.has(item.toLowerCase()),
425
- );
426
- }
427
- throw new Error("Invalid pi-sync settings: expected an include array.");
393
+ if (value === undefined) return [...DEFAULT_SYNC_INCLUDE];
394
+ if (Array.isArray(value)) {
395
+ return normalizeSyncInclude(value).filter((item): item is BuiltInSyncFile =>
396
+ BUILT_IN_BY_LOWER.has(item.toLowerCase()),
397
+ );
398
+ }
399
+ throw new Error("Invalid pi-sync settings: expected an include array.");
428
400
  }
429
401
 
430
402
  /** Compatibility projection for old rendering helpers; v3 persistence never writes this split field. */
431
403
  export function normalizeExtraFiles(value: unknown) {
432
- if (!Array.isArray(value)) return [];
433
- return value.filter(
434
- (item): item is string => typeof item === "string" && isSafeCustomIncludePath(item),
435
- );
404
+ if (!Array.isArray(value)) return [];
405
+ return value.filter((item): item is string => typeof item === "string" && isSafeCustomIncludePath(item));
436
406
  }
437
407
 
438
408
  export const extraFilePathsByLower = customIncludePathsByLower;