@narumitw/pi-sync 0.57.0 → 0.57.2

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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{agent-candidate-3JN6DJKT.ts → agent-candidate-GNEYR225.ts} +3 -3
  3. package/dist/chunks/{agent-plan-QCUJWBP4.ts → agent-plan-K3CRYVXV.ts} +3 -3
  4. package/dist/chunks/{chunk-QSHMOL3I.ts → chunk-4TQWFLOQ.ts} +6 -4
  5. package/dist/chunks/chunk-4TQWFLOQ.ts.map +7 -0
  6. package/dist/chunks/{chunk-FQOSHGJ7.ts → chunk-5QAE3G3K.ts} +11 -6
  7. package/dist/chunks/chunk-5QAE3G3K.ts.map +7 -0
  8. package/dist/chunks/{chunk-S2FMH5FT.ts → chunk-QOC47PBC.ts} +2 -2
  9. package/dist/chunks/{chunk-5Q5SSKNG.ts → chunk-S7QS4AWK.ts} +89 -2
  10. package/dist/chunks/chunk-S7QS4AWK.ts.map +7 -0
  11. package/dist/chunks/{chunk-OTZRCKYP.ts → chunk-T3NC77TU.ts} +29 -16
  12. package/dist/chunks/chunk-T3NC77TU.ts.map +7 -0
  13. package/dist/chunks/{conflict-review-6WIEQLXW.ts → conflict-review-ZCGWFKEK.ts} +3 -3
  14. package/dist/chunks/{continuous-sync-AWAAOXK2.ts → continuous-sync-LAIMSW7D.ts} +2 -2
  15. package/dist/chunks/{manager-ui-KRAW3NE2.ts → manager-ui-PSA2L6CE.ts} +2 -2
  16. package/dist/chunks/{setup-wizard-65DROCIJ.ts → setup-wizard-SOYOJQKP.ts} +2 -2
  17. package/dist/chunks/{sync-operations-HDUDT5PD.ts → sync-operations-W5HFFMTP.ts} +3 -3
  18. package/dist/index.ts +6 -6
  19. package/package.json +6 -4
  20. package/skills/configure-pi-sync/references/configuration.md +1 -1
  21. package/src/sync/agent-candidate.ts +8 -3
  22. package/src/sync/merged-sync.ts +1 -1
  23. package/src/sync/package-merge.ts +87 -0
  24. package/src/sync/settings-conflicts.ts +25 -13
  25. package/src/sync/settings-merge.ts +48 -0
  26. package/src/ui/secret-input.ts +5 -3
  27. package/dist/chunks/chunk-5Q5SSKNG.ts.map +0 -7
  28. package/dist/chunks/chunk-FQOSHGJ7.ts.map +0 -7
  29. package/dist/chunks/chunk-OTZRCKYP.ts.map +0 -7
  30. package/dist/chunks/chunk-QSHMOL3I.ts.map +0 -7
  31. /package/dist/chunks/{agent-candidate-3JN6DJKT.ts.map → agent-candidate-GNEYR225.ts.map} +0 -0
  32. /package/dist/chunks/{agent-plan-QCUJWBP4.ts.map → agent-plan-K3CRYVXV.ts.map} +0 -0
  33. /package/dist/chunks/{chunk-S2FMH5FT.ts.map → chunk-QOC47PBC.ts.map} +0 -0
  34. /package/dist/chunks/{conflict-review-6WIEQLXW.ts.map → conflict-review-ZCGWFKEK.ts.map} +0 -0
  35. /package/dist/chunks/{continuous-sync-AWAAOXK2.ts.map → continuous-sync-LAIMSW7D.ts.map} +0 -0
  36. /package/dist/chunks/{manager-ui-KRAW3NE2.ts.map → manager-ui-PSA2L6CE.ts.map} +0 -0
  37. /package/dist/chunks/{setup-wizard-65DROCIJ.ts.map → setup-wizard-SOYOJQKP.ts.map} +0 -0
  38. /package/dist/chunks/{sync-operations-HDUDT5PD.ts.map → sync-operations-W5HFFMTP.ts.map} +0 -0
package/dist/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
4
  withAgentSyncLock
5
- } from "./chunks/chunk-S2FMH5FT.ts";
5
+ } from "./chunks/chunk-QOC47PBC.ts";
6
6
  import {
7
7
  recoverSnapshotTransactionsOnStartup
8
8
  } from "./chunks/chunk-CRKTAPSL.ts";
@@ -215,7 +215,7 @@ async function executeCommand(rawArgs, ctx, signal, loaders, onCommit, setup, on
215
215
  return { kind: "completed" };
216
216
  }
217
217
  case "conflicts": {
218
- const { showConflicts } = await import("./chunks/conflict-review-6WIEQLXW.ts");
218
+ const { showConflicts } = await import("./chunks/conflict-review-ZCGWFKEK.ts");
219
219
  throwIfAborted(options.signal);
220
220
  await withLock("sync", () => showConflicts(ctx, options));
221
221
  return { kind: "completed" };
@@ -312,7 +312,7 @@ async function initConfig(ctx, signal) {
312
312
  return;
313
313
  }
314
314
  if (ctx.mode === "tui") {
315
- const { showSetupWizard } = await import("./chunks/setup-wizard-65DROCIJ.ts");
315
+ const { showSetupWizard } = await import("./chunks/setup-wizard-SOYOJQKP.ts");
316
316
  throwIfAborted(signal);
317
317
  await showSetupWizard(ctx, signal);
318
318
  return;
@@ -429,7 +429,7 @@ async function handleCommand(rawArgs, ctx, sessionSignal, loaders, attention, on
429
429
  };
430
430
  if (!rawArgs.trim()) {
431
431
  try {
432
- const { showSyncManager } = await import("./chunks/manager-ui-KRAW3NE2.ts");
432
+ const { showSyncManager } = await import("./chunks/manager-ui-PSA2L6CE.ts");
433
433
  if (sessionSignal.aborted) return;
434
434
  await showSyncManager(ctx, run, sessionSignal, {
435
435
  getAttention: () => attention.current(),
@@ -844,7 +844,7 @@ function createSyncLoaders(dependencies) {
844
844
  setupSwitch: cachedModuleLoader(dependencies.loadSetupSwitch ?? (() => import("./chunks/setup-switch-E7PHKI6J.ts"))),
845
845
  snapshot: cachedModuleLoader(dependencies.loadSnapshot ?? (() => import("./chunks/snapshot-THS5EY2L.ts"))),
846
846
  syncState: cachedModuleLoader(dependencies.loadSyncState ?? (() => import("./chunks/sync-state-CFL4RRRB.ts"))),
847
- operations: cachedModuleLoader(dependencies.loadSyncOperations ?? (() => import("./chunks/sync-operations-HDUDT5PD.ts")))
847
+ operations: cachedModuleLoader(dependencies.loadSyncOperations ?? (() => import("./chunks/sync-operations-W5HFFMTP.ts")))
848
848
  };
849
849
  return loaders;
850
850
  }
@@ -886,7 +886,7 @@ function sync(pi, dependencies = {}) {
886
886
  await continuous?.stop(ctx.sessionManager);
887
887
  return;
888
888
  }
889
- const { createContinuousSync } = await import("./chunks/continuous-sync-AWAAOXK2.ts");
889
+ const { createContinuousSync } = await import("./chunks/continuous-sync-LAIMSW7D.ts");
890
890
  if (!isCurrent()) return;
891
891
  continuous ??= createContinuousSync(loaders);
892
892
  continuous.start(ctx, signal, Boolean(retry && (retry.setup === void 0 || retry.setup === config.setupName)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.57.0",
3
+ "version": "0.57.2",
4
4
  "description": "Pi extension that syncs Pi configuration through Git, WebDAV, Cloudflare R2, or S3-compatible storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,12 +47,14 @@
47
47
  "dependencies": {
48
48
  "@narumitw/pi-tui-kit": "^0.65.4",
49
49
  "fast-xml-parser": "^5.11.2",
50
- "proper-lockfile": "^4.1.2"
50
+ "proper-lockfile": "^4.1.2",
51
+ "semver": "^7.8.5"
51
52
  },
52
53
  "devDependencies": {
53
- "@earendil-works/pi-coding-agent": "1.2.0",
54
- "@earendil-works/pi-tui": "1.2.0",
54
+ "@earendil-works/pi-coding-agent": "1.3.0",
55
+ "@earendil-works/pi-tui": "1.3.0",
55
56
  "@types/proper-lockfile": "^4.1.4",
57
+ "@types/semver": "^7.8.0",
56
58
  "esbuild": "0.28.2",
57
59
  "typescript": "7.0.2"
58
60
  },
@@ -367,7 +367,7 @@ Parent model prompts and tools are not changed.
367
367
  This is configuration isolation, not an OS sandbox or proof of semantic correctness.
368
368
 
369
369
  Eligible text is global `AGENTS.md` and `.md`/`.txt` files under `prompts/` or `skills/`.
370
- Global `settings.json` uses the existing strict JSON, atomic-value, numeric, coupled-field, unknown-field, and machine-local-field policies.
370
+ Global `settings.json` uses the existing strict JSON, numeric, coupled-field, unknown-field, and machine-local-field policies. In Agent mode, `packages` merges by source identity and selects the higher explicit SemVer version for each npm package present on both sides, even when one side has not changed from the verified ancestor. No registry lookup occurs. Equal SemVer precedence does not select a winner: one-sided source edits are preserved, and divergent edits require atomic selection. Package filters must match. An existing local or remote array keeps its JSON spelling when it matches the merged result. Accepted settings files keep their bytes unless a package override is required; that override changes only the `packages` value. The ordered npm identity list can change on one side, or identically on both sides; conflicting order or membership changes retain atomic selection. This preserves insertion positions and resource precedence. Non-npm entries must remain unchanged across all three versions for per-package merging. Changed Git refs and local path aliases retain atomic selection instead of being combined as separate sources. Delete/upgrade conflicts, duplicate identities, incompatible filters, version ranges and tags also retain the existing atomic selection rules when deterministic merging is not possible. Other arrays and nested objects remain atomic.
371
371
  Code/scripts, keybindings, binaries, unknown formats, session histories, deletion/modification conflicts, path collisions, and session-root transitions are not model-resolved.
372
372
  Existing deterministic merges still follow their own explicit settings.
373
373
  Unsupported or refused groups remain unresolved; only an explicitly enabled `partialSync` policy permits progress around them.
@@ -109,9 +109,13 @@ export function buildAgentEvidence(
109
109
  let units: AgentUnit[];
110
110
  if (conflict.path === "settings.json") {
111
111
  const documents = versions.map((value) => parseSettingsDocument(Buffer.from(value)));
112
- const merge = mergeSettingsJson(...(versions.map((value) => Buffer.from(value)) as [Buffer, Buffer, Buffer]));
113
- if (merge.kind !== "review" || merge.reason !== "field-conflict") throw new Error("unsupported settings");
114
- const fields = new Set(merge.fields);
112
+ const merge = mergeSettingsJson(
113
+ ...(versions.map((value) => Buffer.from(value)) as [Buffer, Buffer, Buffer]),
114
+ undefined,
115
+ true,
116
+ );
117
+ if (merge.kind === "review" && merge.reason !== "field-conflict") throw new Error("unsupported settings");
118
+ const fields = new Set(merge.kind === "review" ? merge.fields : []);
115
119
  const groups = COUPLED_FIELDS.map((group) => [...group] as string[]);
116
120
  const coupled = new Set(groups.flat());
117
121
  groups.push(...[...fields].filter((key) => !coupled.has(key)).map((key) => [key]));
@@ -229,6 +233,7 @@ export function applyAgentCandidate(
229
233
  Buffer.from(file.local),
230
234
  Buffer.from(file.remote),
231
235
  selections,
236
+ true,
232
237
  );
233
238
  if (merged.kind !== "merged") throw new Error("Agent settings units remain unresolved.");
234
239
  // The existing renderer/parser retains atomic values, unknown members and exact numeric policy.
@@ -289,7 +289,7 @@ export async function mergeSync(
289
289
  );
290
290
  await validate();
291
291
  let resolved = { plan, fields: [] as string[] };
292
- if (config.mergeSettings) {
292
+ if (config.mergeSettings || config.agentMode) {
293
293
  try {
294
294
  resolved = await timings.measure("transfer settings merge", () =>
295
295
  resolveSettingsConflicts(config, state, local, remote, plan),
@@ -0,0 +1,87 @@
1
+ import { compare, valid } from "semver";
2
+ import { type JsonValue, jsonEqual } from "./json-document.js";
3
+
4
+ interface Entry {
5
+ value: JsonValue;
6
+ version?: string;
7
+ options: JsonValue;
8
+ }
9
+
10
+ function entries(value: JsonValue | undefined): Map<string, Entry> | undefined {
11
+ if (!Array.isArray(value)) return undefined;
12
+ const result = new Map<string, Entry>();
13
+ for (const item of value) {
14
+ if (typeof item !== "string" && (!item || Array.isArray(item) || typeof item !== "object")) return undefined;
15
+ const source = typeof item === "string" ? item : item.source;
16
+ if (typeof source !== "string") return undefined;
17
+ const spec = source.startsWith("npm:") ? source.slice(4).trim() : undefined;
18
+ const match = spec?.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/u);
19
+ const key = match ? `npm:${match[1]}` : source;
20
+ if (result.has(key)) return undefined;
21
+ const options =
22
+ typeof item === "string" ? null : Object.fromEntries(Object.entries(item).filter(([key]) => key !== "source"));
23
+ result.set(key, { value: item, options, version: match?.[2] });
24
+ }
25
+ return result;
26
+ }
27
+
28
+ /** Compare only explicit npm versions; do not resolve ranges, tags, Git refs or registry state. */
29
+ export function mergePackageVersions(
30
+ base: JsonValue | undefined,
31
+ local: JsonValue | undefined,
32
+ remote: JsonValue | undefined,
33
+ ): JsonValue[] | undefined {
34
+ const ancestor = entries(base ?? []);
35
+ const left = entries(local);
36
+ const right = entries(remote);
37
+ if (!ancestor || !left || !right) return undefined;
38
+ // Raw Git refs and local path aliases do not prove distinct package identities.
39
+ // Leave changes to those sources under the existing atomic field policy.
40
+ const otherEntries = (items: Map<string, Entry>) =>
41
+ [...items].filter(([key]) => !key.startsWith("npm:")).map(([, entry]) => entry.value);
42
+ if (!jsonEqual(otherEntries(ancestor), otherEntries(left)) || !jsonEqual(otherEntries(ancestor), otherEntries(right)))
43
+ return undefined;
44
+
45
+ // Package order affects resource precedence. Merge the ordered identity list
46
+ // atomically, then select versions without changing that order.
47
+ const baseKeys = [...ancestor.keys()];
48
+ const leftKeys = [...left.keys()];
49
+ const rightKeys = [...right.keys()];
50
+ const order = jsonEqual(leftKeys, rightKeys)
51
+ ? leftKeys
52
+ : jsonEqual(leftKeys, baseKeys)
53
+ ? rightKeys
54
+ : jsonEqual(rightKeys, baseKeys)
55
+ ? leftKeys
56
+ : undefined;
57
+ if (!order) return undefined;
58
+ const output: JsonValue[] = [];
59
+ for (const key of new Set([...order, ...baseKeys])) {
60
+ const a = ancestor.get(key);
61
+ const l = left.get(key);
62
+ const r = right.get(key);
63
+ let chosen: Entry | undefined;
64
+ if (
65
+ l &&
66
+ r &&
67
+ key.startsWith("npm:") &&
68
+ l.version &&
69
+ r.version &&
70
+ valid(l.version) &&
71
+ valid(r.version) &&
72
+ jsonEqual(l.options, r.options)
73
+ ) {
74
+ const precedence = compare(l.version, r.version);
75
+ if (precedence !== 0) chosen = precedence > 0 ? l : r;
76
+ }
77
+ // Build metadata and equivalent version spelling do not establish a higher version.
78
+ if (!chosen) {
79
+ if (jsonEqual(l?.value, r?.value)) chosen = l;
80
+ else if (jsonEqual(l?.value, a?.value)) chosen = r;
81
+ else if (jsonEqual(r?.value, a?.value)) chosen = l;
82
+ else return undefined;
83
+ }
84
+ if (chosen) output.push(chosen.value);
85
+ }
86
+ return output;
87
+ }
@@ -4,7 +4,7 @@ import type { Snapshot } from "../snapshot/snapshot-types.js";
4
4
  import { readMergeAncestor } from "../state/merge-baseline-store.js";
5
5
  import type { SyncState } from "../state/state-types.js";
6
6
  import type { FileMergePlan } from "./file-merge-planner.js";
7
- import { mergeSettingsJson } from "./settings-merge.js";
7
+ import { mergeAcceptedSettingsPackages, mergeSettingsJson } from "./settings-merge.js";
8
8
 
9
9
  export async function resolveSettingsConflicts(
10
10
  config: AnySyncConfig,
@@ -18,21 +18,33 @@ export async function resolveSettingsConflicts(
18
18
  const decisions = [...plan.decisions];
19
19
  for (let index = 0; index < decisions.length; index++) {
20
20
  const decision = decisions[index];
21
- if (decision?.kind !== "conflict" || decision.reason !== "both-changed" || decision.path !== "settings.json")
22
- continue;
21
+ if (decision?.path !== "settings.json") continue;
22
+ if (decision.kind === "conflict" ? decision.reason !== "both-changed" : !config.agentMode) continue;
23
23
  const left = local.files.find((file) => file.path === decision.path);
24
24
  const right = remote.files.find((file) => file.path === decision.path);
25
25
  if (!left || !right) continue; // File deletion/addition is not a JSON field merge.
26
26
  const ancestor = await readMergeAncestor(config, state, decision.path);
27
27
  if (!ancestor) continue;
28
- const merged = mergeSettingsJson(
29
- ancestor,
30
- Buffer.from(left.contentBase64, "base64"),
31
- Buffer.from(right.contentBase64, "base64"),
32
- );
33
- if (merged.kind === "review") {
34
- fields.push(...merged.fields);
35
- continue;
28
+ const leftBytes = Buffer.from(left.contentBase64, "base64");
29
+ const rightBytes = Buffer.from(right.contentBase64, "base64");
30
+ let content: Buffer;
31
+ if (decision.kind === "accepted") {
32
+ if (!decision.file) continue;
33
+ const patched = mergeAcceptedSettingsPackages(
34
+ ancestor,
35
+ leftBytes,
36
+ rightBytes,
37
+ Buffer.from(decision.file.contentBase64, "base64"),
38
+ );
39
+ if (!patched) continue;
40
+ content = patched;
41
+ } else {
42
+ const merged = mergeSettingsJson(ancestor, leftBytes, rightBytes, undefined, Boolean(config.agentMode));
43
+ if (merged.kind === "review") {
44
+ fields.push(...merged.fields);
45
+ continue;
46
+ }
47
+ content = merged.content;
36
48
  }
37
49
  decisions[index] = {
38
50
  kind: "accepted",
@@ -40,8 +52,8 @@ export async function resolveSettingsConflicts(
40
52
  source: "merged",
41
53
  file: {
42
54
  path: decision.path,
43
- contentBase64: merged.content.toString("base64"),
44
- sha256: createHash("sha256").update(merged.content).digest("hex"),
55
+ contentBase64: content.toString("base64"),
56
+ sha256: createHash("sha256").update(content).digest("hex"),
45
57
  },
46
58
  };
47
59
  }
@@ -6,6 +6,8 @@ import {
6
6
  renderSettingsMembers,
7
7
  } from "./json-document.js";
8
8
 
9
+ import { mergePackageVersions } from "./package-merge.js";
10
+
9
11
  export type SettingsMergeResult =
10
12
  | { kind: "merged"; content: Buffer }
11
13
  | { kind: "review"; reason: "unsupported-format" | "field-conflict"; fields: string[] };
@@ -18,12 +20,51 @@ export const COUPLED_FIELDS = [
18
20
  ["enableAnalytics", "trackingId"],
19
21
  ] as const;
20
22
 
23
+ function selectPackages(base: JsonDocument, left: JsonDocument, right: JsonDocument) {
24
+ const packages = mergePackageVersions(
25
+ base.root.members.get("packages")?.node.value,
26
+ left.root.members.get("packages")?.node.value,
27
+ right.root.members.get("packages")?.node.value,
28
+ );
29
+ if (!packages) return undefined;
30
+ const document =
31
+ [left, right].find((source) => jsonEqual(source.root.members.get("packages")?.node.value, packages)) ??
32
+ parseSettingsDocument(Buffer.from(JSON.stringify({ packages })));
33
+ const node = document.root.members.get("packages")?.node;
34
+ return node ? { document, node } : undefined;
35
+ }
36
+
37
+ /** Accepted files need only a package override, not another full-document field merge. */
38
+ export function mergeAcceptedSettingsPackages(
39
+ ancestor: Buffer,
40
+ local: Buffer,
41
+ remote: Buffer,
42
+ accepted: Buffer,
43
+ ): Buffer | undefined {
44
+ try {
45
+ const base = parseSettingsDocument(ancestor);
46
+ const left = parseSettingsDocument(local);
47
+ const right = parseSettingsDocument(remote);
48
+ const document = parseSettingsDocument(accepted);
49
+ const packages = selectPackages(base, left, right);
50
+ if (!packages || jsonEqual(packages.node.value, document.root.members.get("packages")?.node.value))
51
+ return undefined;
52
+ const selected = new Map([...document.root.members].map(([key, member]) => [key, { document, node: member.node }]));
53
+ selected.set("packages", packages);
54
+ return renderSettingsMembers(document, selected);
55
+ } catch {
56
+ // Unsupported settings must not replace an accepted file with a reconstructed document.
57
+ return undefined;
58
+ }
59
+ }
60
+
21
61
  /** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */
22
62
  export function mergeSettingsJson(
23
63
  ancestor: Buffer,
24
64
  local: Buffer,
25
65
  remote: Buffer,
26
66
  selections: ReadonlyMap<string, "local" | "remote"> = new Map(),
67
+ preferHigherPackageVersions = false,
27
68
  ): SettingsMergeResult {
28
69
  let documents: JsonDocument[];
29
70
  try {
@@ -39,6 +80,13 @@ export function mergeSettingsJson(
39
80
  units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));
40
81
  const conflicts: string[] = [];
41
82
  for (const unit of units) {
83
+ if (preferHigherPackageVersions && unit[0] === "packages") {
84
+ const packages = selectPackages(base, left, right);
85
+ if (packages) {
86
+ selected.set("packages", packages);
87
+ continue;
88
+ }
89
+ }
42
90
  const equal = (first: JsonDocument, second: JsonDocument) =>
43
91
  unit.every((field) =>
44
92
  jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),
@@ -6,6 +6,7 @@ import {
6
6
  Key,
7
7
  type KeybindingsManager,
8
8
  matchesKey,
9
+ renderFakeCursor,
9
10
  Text,
10
11
  truncateToWidth,
11
12
  } from "@earendil-works/pi-tui";
@@ -54,7 +55,8 @@ export async function promptSecret(
54
55
  input.focused = focused;
55
56
  },
56
57
  render(width: number) {
57
- const safeWidth = Math.max(1, width);
58
+ const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
59
+ if (safeWidth === 0) return [];
58
60
  return [...heading.render(safeWidth), ...input.render(safeWidth), ...hint.render(safeWidth)].map((line) =>
59
61
  truncateToWidth(line, safeWidth),
60
62
  );
@@ -179,7 +181,7 @@ class MaskedInput implements Focusable {
179
181
  render(width: number) {
180
182
  const prompt = "> ";
181
183
  const available = width - prompt.length;
182
- if (available <= 0) return [truncateToWidth(prompt, Math.max(1, width))];
184
+ if (available <= 0) return [truncateToWidth(prompt, Math.max(0, width))];
183
185
  const contentWidth = Math.max(0, available - 1);
184
186
  let start = 0;
185
187
  if (this.value.length > contentWidth) {
@@ -192,7 +194,7 @@ class MaskedInput implements Focusable {
192
194
  const atCursor = visibleCursor < masks.length ? MASK : " ";
193
195
  const after = masks.slice(visibleCursor + (visibleCursor < masks.length ? 1 : 0)).join("");
194
196
  const marker = this.focused ? CURSOR_MARKER : "";
195
- const line = `${prompt}${before}${marker}\u001b[7m${atCursor}\u001b[27m${after}`;
197
+ const line = `${prompt}${before}${marker}${renderFakeCursor(atCursor)}${after}`;
196
198
  return [truncateToWidth(line, width, "")];
197
199
  }
198
200
 
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/sync/content-conflicts.ts", "../../src/sync/settings-merge.ts"],
4
- "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport type { Snapshot, SnapshotFile } from \"../snapshot/snapshot-types.js\";\nimport { readMergeAncestors } from \"../state/merge-baseline-store.js\";\nimport type { SyncState } from \"../state/state-types.js\";\nimport { type FileMergePlan, mergePathIdentity } from \"./file-merge-planner.js\";\nimport { mergeSession } from \"./session-merge.js\";\nimport { isMergeTextPath, mergeText, TEXT_MERGE_WORK_LIMIT, type TextMergeBudget } from \"./text-merge.js\";\nexport function snapshotFile(filePath: string, bytes: Buffer): SnapshotFile {\n return {\n path: filePath,\n contentBase64: bytes.toString(\"base64\"),\n sha256: createHash(\"sha256\").update(bytes).digest(\"hex\"),\n };\n}\nexport async function resolveContentConflicts(\n config: AnySyncConfig,\n state: SyncState,\n local: Snapshot,\n remote: Snapshot,\n plan: FileMergePlan,\n protectedPaths: ReadonlySet<string>,\n options: { textBudget?: TextMergeBudget; signal?: AbortSignal } = {},\n): Promise<FileMergePlan> {\n if (plan.kind !== \"planned\" || !config.mergeContent) return plan;\n options.signal?.throwIfAborted();\n const budget = options.textBudget ?? { remaining: TEXT_MERGE_WORK_LIMIT };\n const decisions = [...plan.decisions];\n let ancestors: SnapshotFile[] = [];\n try {\n ancestors = (await readMergeAncestors(config, state)) ?? [];\n } catch {\n options.signal?.throwIfAborted();\n return plan;\n }\n options.signal?.throwIfAborted();\n const oursByPath = new Map(local.files.map((file) => [file.path, file]));\n const theirsByPath = new Map(remote.files.map((file) => [file.path, file]));\n const ancestorsByPath = new Map(ancestors.map((file) => [file.path, file]));\n const decisionIndex = new Map(decisions.map((decision, index) => [decision.path, index]));\n const protectedKeys = new Set([...protectedPaths].map(mergePathIdentity));\n for (const conflict of plan.conflicts) {\n options.signal?.throwIfAborted();\n if (conflict.reason !== \"both-changed\" || protectedKeys.has(mergePathIdentity(conflict.path))) continue;\n const session = conflict.path.startsWith(\"sessions/\") && conflict.path.endsWith(\".jsonl\");\n if (!session && (!isMergeTextPath(conflict.path) || budget.remaining === 0)) continue;\n const ours = oursByPath.get(conflict.path);\n const theirs = theirsByPath.get(conflict.path);\n if (!ours || !theirs) continue;\n const ancestor = ancestorsByPath.get(conflict.path);\n const base = ancestor ? Buffer.from(ancestor.contentBase64, \"base64\") : undefined;\n if (!base) continue;\n const localBytes = Buffer.from(ours.contentBase64, \"base64\");\n const remoteBytes = Buffer.from(theirs.contentBase64, \"base64\");\n const bytes = session\n ? mergeSession(base, localBytes, remoteBytes)\n : mergeText(base, localBytes, remoteBytes, { budget, signal: options.signal });\n if (!bytes) continue;\n const index = decisionIndex.get(conflict.path);\n if (index === undefined) continue;\n decisions[index] = {\n kind: \"accepted\",\n path: conflict.path,\n source: \"merged\",\n file: snapshotFile(conflict.path, bytes),\n };\n }\n return {\n kind: \"planned\",\n decisions,\n conflicts: decisions.filter((item): item is Extract<typeof item, { kind: \"conflict\" }> => item.kind === \"conflict\"),\n };\n}\n", "import {\n type JsonDocument,\n type JsonNode,\n jsonEqual,\n parseSettingsDocument,\n renderSettingsMembers,\n} from \"./json-document.js\";\n\nexport type SettingsMergeResult =\n | { kind: \"merged\"; content: Buffer }\n | { kind: \"review\"; reason: \"unsupported-format\" | \"field-conflict\"; fields: string[] };\n\nexport const COUPLED_FIELDS = [\n [\"defaultProvider\", \"defaultModel\"],\n [\"queueMode\", \"steeringMode\"],\n [\"websockets\", \"transport\"],\n [\"skills\", \"enableSkillCommands\"],\n [\"enableAnalytics\", \"trackingId\"],\n] as const;\n\n/** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */\nexport function mergeSettingsJson(\n ancestor: Buffer,\n local: Buffer,\n remote: Buffer,\n selections: ReadonlyMap<string, \"local\" | \"remote\"> = new Map(),\n): SettingsMergeResult {\n let documents: JsonDocument[];\n try {\n documents = [ancestor, local, remote].map(parseSettingsDocument);\n } catch {\n return { kind: \"review\", reason: \"unsupported-format\", fields: [] };\n }\n const [base, left, right] = documents as [JsonDocument, JsonDocument, JsonDocument];\n const selected = new Map<string, { document: JsonDocument; node: JsonNode }>();\n const fields = new Set(documents.flatMap((document) => [...document.root.members.keys()]));\n const units: string[][] = COUPLED_FIELDS.map((group) => [...group]);\n const coupled = new Set(units.flat());\n units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));\n const conflicts: string[] = [];\n for (const unit of units) {\n const equal = (first: JsonDocument, second: JsonDocument) =>\n unit.every((field) =>\n jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),\n );\n const choice = selections.get(unit[0] ?? \"\");\n const source = equal(left, right)\n ? left\n : equal(left, base)\n ? right\n : equal(right, base)\n ? left\n : choice === \"local\"\n ? left\n : choice === \"remote\"\n ? right\n : undefined;\n if (!source) {\n conflicts.push(...unit.filter((field) => fields.has(field)));\n continue;\n }\n for (const field of unit) {\n const member = source.root.members.get(field);\n if (member) selected.set(field, { document: source, node: member.node });\n }\n }\n if (conflicts.length) return { kind: \"review\", reason: \"field-conflict\", fields: conflicts };\n try {\n return { kind: \"merged\", content: renderSettingsMembers(left, selected) };\n } catch {\n return { kind: \"review\", reason: \"unsupported-format\", fields: [] };\n }\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/sync/agent-candidate.ts"],
4
- "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { scanSnapshot } from \"../snapshot/snapshot.js\";\nimport type { Snapshot, SnapshotFile } from \"../snapshot/snapshot-types.js\";\nimport { AGENT_INPUT_BYTES, AGENT_MAX_UNITS, AGENT_OUTPUT_BYTES } from \"./agent-policy.js\";\nimport { snapshotFile } from \"./content-conflicts.js\";\nimport type { FileMergePlan } from \"./file-merge-planner.js\";\nimport { parseJsonObjectDocument, parseSettingsDocument } from \"./json-document.js\";\nimport { COUPLED_FIELDS, mergeSettingsJson } from \"./settings-merge.js\";\nimport { edits, type Hunk, isMergeTextPath, lines, text } from \"./text-merge.js\";\n\nexport interface AgentUnit {\n id: string;\n path: string;\n kind: \"text\" | \"settings\";\n base: string;\n local: string;\n remote: string;\n}\ninterface CandidateFile {\n path: string;\n base: string;\n local: string;\n remote: string;\n parts?: (string | AgentUnit)[];\n fields?: { id: string; key: string }[];\n}\nexport interface AgentEvidence {\n units: AgentUnit[];\n files: CandidateFile[];\n unsupported: string[];\n}\nexport interface AgentCandidate {\n version: 1;\n results: (\n | { id: string; content: string }\n | { id: string; source: \"local\" | \"remote\" }\n | { id: string; refuse: true }\n )[];\n}\nexport const digest = (value: unknown) => createHash(\"sha256\").update(JSON.stringify(value)).digest(\"hex\");\n\nfunction overlaps(a: Hunk, b: Hunk) {\n return a.start === a.end || b.start === b.end\n ? a.start <= b.end && b.start <= a.end\n : a.start < b.end && b.start < a.end;\n}\n\n/** Only overlapping edit regions are model-writable; all other bytes are assembled by code. */\nfunction textParts(filePath: string, baseText: string, localText: string, remoteText: string): (string | AgentUnit)[] {\n const base = lines(baseText);\n const changes = [\n ...edits(base, lines(localText)).map((hunk) => ({ ...hunk, side: \"local\" as const })),\n ...edits(base, lines(remoteText)).map((hunk) => ({ ...hunk, side: \"remote\" as const })),\n ].sort((a, b) => a.start - b.start || a.end - b.end);\n const clusters: (typeof changes)[] = [];\n for (const change of changes) {\n const last = clusters.at(-1);\n if (last?.some((item) => overlaps(item, change))) last.push(change);\n else clusters.push([change]);\n }\n const result: (string | AgentUnit)[] = [];\n let cursor = 0;\n for (const cluster of clusters) {\n const start = Math.min(...cluster.map((item) => item.start));\n const end = Math.max(...cluster.map((item) => item.end));\n result.push(base.slice(cursor, start).join(\"\"));\n const image = (side: \"local\" | \"remote\") => {\n let position = start;\n const output: string[] = [];\n for (const hunk of cluster.filter((item) => item.side === side)) {\n output.push(...base.slice(position, hunk.start), ...hunk.lines);\n position = hunk.end;\n }\n return [...output, ...base.slice(position, end)].join(\"\");\n };\n const local = image(\"local\");\n const remote = image(\"remote\");\n const original = base.slice(start, end).join(\"\");\n if (local === remote || local === original || remote === original) result.push(local === original ? remote : local);\n else result.push({ id: \"\", path: filePath, kind: \"text\", base: original, local, remote });\n cursor = end;\n }\n result.push(base.slice(cursor).join(\"\"));\n return result;\n}\n\nexport function buildAgentEvidence(\n plan: FileMergePlan,\n local: Snapshot,\n remote: Snapshot,\n ancestors: SnapshotFile[],\n): AgentEvidence {\n const evidence: AgentEvidence = { units: [], files: [], unsupported: [] };\n if (plan.kind !== \"planned\") return evidence;\n if (plan.conflicts.length > AGENT_MAX_UNITS)\n throw new Error(\"Agent mode paused: too many conflict paths for bounded analysis.\");\n for (const conflict of plan.conflicts) {\n try {\n if (conflict.reason !== \"both-changed\") throw new Error(\"unsupported conflict\");\n const versions = [ancestors, local.files, remote.files].map((files) => {\n const file = files.find((item) => item.path === conflict.path);\n if (!file) throw new Error(\"missing ancestor/version\");\n const bytes = Buffer.from(file.contentBase64, \"base64\");\n if (bytes.length > 8192) throw new Error(\"file bound\");\n return text(bytes);\n }) as [string, string, string];\n const [base, left, right] = versions;\n const file: CandidateFile = { path: conflict.path, base, local: left, remote: right };\n let units: AgentUnit[];\n if (conflict.path === \"settings.json\") {\n const documents = versions.map((value) => parseSettingsDocument(Buffer.from(value)));\n const merge = mergeSettingsJson(...(versions.map((value) => Buffer.from(value)) as [Buffer, Buffer, Buffer]));\n if (merge.kind !== \"review\" || merge.reason !== \"field-conflict\") throw new Error(\"unsupported settings\");\n const fields = new Set(merge.fields);\n const groups = COUPLED_FIELDS.map((group) => [...group] as string[]);\n const coupled = new Set(groups.flat());\n groups.push(...[...fields].filter((key) => !coupled.has(key)).map((key) => [key]));\n file.fields = [];\n units = groups\n .filter((group) => group.some((key) => fields.has(key)))\n .map((group) => {\n const values = documents.map((document) =>\n JSON.stringify(\n Object.fromEntries(\n group.flatMap((key) => {\n const node = document.root.members.get(key)?.node;\n return node ? [[key, node.value]] : [];\n }),\n ),\n ),\n );\n const unit: AgentUnit = {\n id: \"\",\n path: file.path,\n kind: \"settings\",\n base: values[0] ?? \"{}\",\n local: values[1] ?? \"{}\",\n remote: values[2] ?? \"{}\",\n };\n file.fields?.push({ id: String(evidence.units.length + file.fields.length), key: group[0] ?? \"\" });\n return unit;\n });\n } else {\n if (!isMergeTextPath(conflict.path)) throw new Error(\"unsupported path\");\n file.parts = textParts(file.path, base, left, right);\n units = file.parts.filter((part): part is AgentUnit => typeof part !== \"string\");\n }\n for (const unit of units) {\n unit.id = String(evidence.units.length);\n evidence.units.push(unit);\n }\n evidence.files.push(file);\n } catch {\n evidence.unsupported.push(conflict.path);\n }\n }\n if (evidence.units.length > AGENT_MAX_UNITS || Buffer.byteLength(JSON.stringify(evidence.units)) > AGENT_INPUT_BYTES)\n throw new Error(\"Agent mode paused: conflict evidence exceeds its input bound.\");\n // Scan only the actual disclosure image; skipSecretScan never authorizes model disclosure.\n if (\n scanSnapshot({\n ...local,\n files: evidence.units.flatMap((unit) =>\n [unit.path, unit.base, unit.local, unit.remote].map((value) => snapshotFile(unit.path, Buffer.from(value))),\n ),\n }).length\n )\n throw new Error(\"Agent mode paused: possible secrets in model evidence; nothing was disclosed.\");\n return evidence;\n}\n\nexport function parseAgentCandidate(raw: string, evidence: AgentEvidence): AgentCandidate {\n if (Buffer.byteLength(raw) > AGENT_OUTPUT_BYTES) throw new Error(\"Agent output exceeds its bound.\");\n const parsed = parseJsonObjectDocument(Buffer.from(raw)).root.value as Record<string, unknown>;\n if (\n Object.keys(parsed).sort().join(\",\") !== \"results,version\" ||\n parsed.version !== 1 ||\n !Array.isArray(parsed.results) ||\n parsed.results.length !== evidence.units.length\n )\n throw new Error(\"Invalid agent candidate contract.\");\n const seen = new Set<string>();\n for (const result of parsed.results) {\n if (!result || typeof result !== \"object\" || Array.isArray(result)) throw new Error(\"Invalid agent result.\");\n const row = result as Record<string, unknown>;\n const unit = evidence.units.find((unit) => unit.id === row.id);\n if (!unit || seen.has(unit.id)) throw new Error(\"Unowned or duplicate agent unit.\");\n seen.add(unit.id);\n const keys = Object.keys(row).sort().join(\",\");\n if (keys === \"id,refuse\" && row.refuse === true) continue;\n if (unit.kind === \"settings\" && keys === \"id,source\" && (row.source === \"local\" || row.source === \"remote\"))\n continue;\n if (unit.kind === \"text\" && keys === \"content,id\" && typeof row.content === \"string\" && row.content.length > 0) {\n text(Buffer.from(row.content));\n if (/^(?:<{7}|={7}|>{7}|\\|{7})/mu.test(row.content)) throw new Error(\"Unresolved conflict markers.\");\n continue;\n }\n throw new Error(\"Unsupported agent action.\");\n }\n return parsed as unknown as AgentCandidate;\n}\n\nexport function applyAgentCandidate(\n plan: FileMergePlan,\n evidence: AgentEvidence,\n candidate: AgentCandidate,\n): FileMergePlan {\n // Persisted candidates cross a trust boundary too.\n parseAgentCandidate(JSON.stringify(candidate), evidence);\n if (plan.kind !== \"planned\") throw new Error(\"Unverified merge plan.\");\n const results = new Map(candidate.results.map((row) => [row.id, row]));\n const replacements = new Map<string, SnapshotFile>();\n for (const file of evidence.files) {\n const units = evidence.units.filter((unit) => unit.path === file.path);\n if (units.some((unit) => results.get(unit.id) && \"refuse\" in (results.get(unit.id) ?? {}))) continue;\n let bytes: Buffer;\n if (file.parts) {\n bytes = Buffer.from(\n file.parts\n .map((part) => (typeof part === \"string\" ? part : (results.get(part.id) as { content: string }).content))\n .join(\"\"),\n );\n } else {\n const selections = new Map(\n (file.fields ?? []).map(({ id, key }) => [key, (results.get(id) as { source: \"local\" | \"remote\" }).source]),\n );\n const merged = mergeSettingsJson(\n Buffer.from(file.base),\n Buffer.from(file.local),\n Buffer.from(file.remote),\n selections,\n );\n if (merged.kind !== \"merged\") throw new Error(\"Agent settings units remain unresolved.\");\n // The existing renderer/parser retains atomic values, unknown members and exact numeric policy.\n parseSettingsDocument(merged.content);\n for (const { key } of file.fields ?? []) {\n if (!selections.has(key)) throw new Error(\"Missing atomic settings selection.\");\n }\n bytes = merged.content;\n }\n if (bytes.length > AGENT_OUTPUT_BYTES) throw new Error(\"Agent file exceeds output bound.\");\n replacements.set(file.path, snapshotFile(file.path, bytes));\n }\n const decisions = plan.decisions.map((decision) => {\n const file = replacements.get(decision.path);\n return file && decision.kind === \"conflict\"\n ? { kind: \"accepted\" as const, path: file.path, source: \"merged\" as const, file }\n : decision;\n });\n return { kind: \"planned\", decisions, conflicts: decisions.filter((item) => item.kind === \"conflict\") };\n}\n"],
5
- "mappings": "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",
6
- "names": ["file", "unit"]
7
- }