@infinite-room-labs/claudesync-core 0.3.0 → 0.4.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 (64) hide show
  1. package/dist/client/client.d.ts +3 -1
  2. package/dist/client/client.d.ts.map +1 -1
  3. package/dist/client/client.js +2 -2
  4. package/dist/client/client.js.map +1 -1
  5. package/dist/client/endpoints.d.ts +3 -1
  6. package/dist/client/endpoints.d.ts.map +1 -1
  7. package/dist/client/endpoints.js +1 -1
  8. package/dist/client/endpoints.js.map +1 -1
  9. package/dist/export/__tests__/bundle-builder.test.js +23 -0
  10. package/dist/export/__tests__/bundle-builder.test.js.map +1 -1
  11. package/dist/export/__tests__/git-exporter.test.d.ts +2 -0
  12. package/dist/export/__tests__/git-exporter.test.d.ts.map +1 -0
  13. package/dist/export/__tests__/git-exporter.test.js +115 -0
  14. package/dist/export/__tests__/git-exporter.test.js.map +1 -0
  15. package/dist/export/bundle-builder.d.ts +15 -7
  16. package/dist/export/bundle-builder.d.ts.map +1 -1
  17. package/dist/export/bundle-builder.js +71 -26
  18. package/dist/export/bundle-builder.js.map +1 -1
  19. package/dist/export/git-exporter.d.ts +20 -4
  20. package/dist/export/git-exporter.d.ts.map +1 -1
  21. package/dist/export/git-exporter.js +188 -39
  22. package/dist/export/git-exporter.js.map +1 -1
  23. package/dist/index.d.ts +9 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/models/schemas.d.ts +24 -24
  28. package/dist/models/schemas.js +3 -3
  29. package/dist/models/schemas.js.map +1 -1
  30. package/dist/sync/__tests__/changelog.test.d.ts +2 -0
  31. package/dist/sync/__tests__/changelog.test.d.ts.map +1 -0
  32. package/dist/sync/__tests__/changelog.test.js +114 -0
  33. package/dist/sync/__tests__/changelog.test.js.map +1 -0
  34. package/dist/sync/__tests__/diff.test.d.ts +2 -0
  35. package/dist/sync/__tests__/diff.test.d.ts.map +1 -0
  36. package/dist/sync/__tests__/diff.test.js +154 -0
  37. package/dist/sync/__tests__/diff.test.js.map +1 -0
  38. package/dist/sync/__tests__/state.test.d.ts +2 -0
  39. package/dist/sync/__tests__/state.test.d.ts.map +1 -0
  40. package/dist/sync/__tests__/state.test.js +53 -0
  41. package/dist/sync/__tests__/state.test.js.map +1 -0
  42. package/dist/sync/changelog.d.ts +19 -0
  43. package/dist/sync/changelog.d.ts.map +1 -0
  44. package/dist/sync/changelog.js +138 -0
  45. package/dist/sync/changelog.js.map +1 -0
  46. package/dist/sync/diff.d.ts +66 -0
  47. package/dist/sync/diff.d.ts.map +1 -0
  48. package/dist/sync/diff.js +125 -0
  49. package/dist/sync/diff.js.map +1 -0
  50. package/dist/sync/incremental.d.ts +36 -0
  51. package/dist/sync/incremental.d.ts.map +1 -0
  52. package/dist/sync/incremental.js +229 -0
  53. package/dist/sync/incremental.js.map +1 -0
  54. package/dist/sync/state.d.ts +108 -0
  55. package/dist/sync/state.d.ts.map +1 -0
  56. package/dist/sync/state.js +52 -0
  57. package/dist/sync/state.js.map +1 -0
  58. package/dist/tree/__tests__/message-tree.test.js +61 -1
  59. package/dist/tree/__tests__/message-tree.test.js.map +1 -1
  60. package/dist/tree/message-tree.d.ts +19 -0
  61. package/dist/tree/message-tree.d.ts.map +1 -1
  62. package/dist/tree/message-tree.js +50 -0
  63. package/dist/tree/message-tree.js.map +1 -1
  64. package/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ import { buildMessageTree, getAllBranches, shortLeafLabel, } from "../tree/message-tree.js";
2
+ /**
3
+ * Diffs a freshly fetched conversation (with full message tree from
4
+ * ?tree=True) and its current artifact list against a previously stored
5
+ * SyncState.
6
+ *
7
+ * If prevState is undefined the result describes an "initial" sync: every
8
+ * branch is new, every artifact is added.
9
+ */
10
+ export function diffConversation(prevState, conversation, artifacts) {
11
+ const nodeMap = buildMessageTree(conversation.chat_messages);
12
+ const branchMap = getAllBranches(nodeMap);
13
+ const allLeafUuids = Array.from(branchMap.keys());
14
+ const prevLeaves = new Map();
15
+ if (prevState) {
16
+ for (const l of prevState.leaves) {
17
+ prevLeaves.set(l.uuid, l.last_message_index);
18
+ }
19
+ }
20
+ // For each current leaf, find the deepest ancestor that was a previous
21
+ // leaf. If found, the current branch is an "extension" of that previous
22
+ // branch (same conceptual branch, new messages). If not, it's a brand-new
23
+ // branch. This avoids flagging "Branch main discovered" every time the
24
+ // current leaf moves forward.
25
+ const branches = [];
26
+ for (const [leafUuid, messages] of branchMap) {
27
+ const exactMatchIndex = prevLeaves.get(leafUuid);
28
+ let predecessorLeafUuid;
29
+ let predecessorIndex;
30
+ if (exactMatchIndex !== undefined) {
31
+ predecessorLeafUuid = leafUuid;
32
+ predecessorIndex = exactMatchIndex;
33
+ }
34
+ else {
35
+ // Walk root->leaf and pick the deepest ancestor that was a previous leaf.
36
+ for (const m of messages) {
37
+ const idx = prevLeaves.get(m.uuid);
38
+ if (idx !== undefined) {
39
+ predecessorLeafUuid = m.uuid;
40
+ predecessorIndex = idx;
41
+ }
42
+ }
43
+ }
44
+ const leafMsg = messages[messages.length - 1];
45
+ const currentIndex = leafMsg?.index ?? -1;
46
+ const isNew = predecessorLeafUuid === undefined;
47
+ const hasNewMessages = !isNew && currentIndex > (predecessorIndex ?? -1);
48
+ const newMessageIndices = isNew
49
+ ? messages.map((m) => m.index)
50
+ : messages
51
+ .filter((m) => m.index > (predecessorIndex ?? -1))
52
+ .map((m) => m.index);
53
+ branches.push({
54
+ leafUuid,
55
+ shortLabel: shortLeafLabel(leafUuid, allLeafUuids),
56
+ isMain: leafUuid === conversation.current_leaf_message_uuid,
57
+ isNew,
58
+ hasNewMessages,
59
+ newMessageIndices,
60
+ messages,
61
+ });
62
+ }
63
+ // Sort: main first, then newest leaves first.
64
+ branches.sort((a, b) => {
65
+ if (a.isMain !== b.isMain)
66
+ return a.isMain ? -1 : 1;
67
+ const aLast = a.messages[a.messages.length - 1]?.created_at ?? "";
68
+ const bLast = b.messages[b.messages.length - 1]?.created_at ?? "";
69
+ return bLast.localeCompare(aLast);
70
+ });
71
+ // Artifacts
72
+ const prevArtifacts = new Map();
73
+ if (prevState) {
74
+ for (const a of prevState.artifacts) {
75
+ prevArtifacts.set(a.path, { size: a.size, created_at: a.created_at });
76
+ }
77
+ }
78
+ const currentArtifacts = new Map();
79
+ for (const a of artifacts.files_metadata) {
80
+ currentArtifacts.set(a.path, { size: a.size, created_at: a.created_at });
81
+ }
82
+ const added = [];
83
+ const changed = [];
84
+ const removed = [];
85
+ for (const [p, info] of currentArtifacts) {
86
+ const prev = prevArtifacts.get(p);
87
+ if (!prev) {
88
+ added.push({ path: p, ...info });
89
+ }
90
+ else if (prev.size !== info.size || prev.created_at !== info.created_at) {
91
+ changed.push({
92
+ path: p,
93
+ size: info.size,
94
+ created_at: info.created_at,
95
+ prev_size: prev.size,
96
+ prev_created_at: prev.created_at,
97
+ });
98
+ }
99
+ }
100
+ for (const [p, prev] of prevArtifacts) {
101
+ if (!currentArtifacts.has(p)) {
102
+ removed.push({ path: p, size: prev.size, created_at: prev.created_at });
103
+ }
104
+ }
105
+ // Metadata
106
+ const metadata = {};
107
+ if (prevState && prevState.conversation_name !== conversation.name) {
108
+ metadata.renamed = {
109
+ from: prevState.conversation_name,
110
+ to: conversation.name,
111
+ };
112
+ }
113
+ // Note: we do not store the previous model in state v1 (could be added),
114
+ // so model changes are only detected if state has it. Future-proof here.
115
+ const isInitial = prevState === undefined;
116
+ const isUnchanged = !isInitial &&
117
+ branches.every((b) => !b.isNew && !b.hasNewMessages) &&
118
+ added.length === 0 &&
119
+ changed.length === 0 &&
120
+ removed.length === 0 &&
121
+ !metadata.renamed &&
122
+ !metadata.modelChanged;
123
+ return { isInitial, isUnchanged, branches, artifacts: { added, changed, removed }, metadata };
124
+ }
125
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/sync/diff.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,cAAc,GACf,MAAM,yBAAyB,CAAC;AAyCjC;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,SAAgC,EAChC,YAA0B,EAC1B,SAA+B;IAE/B,MAAM,OAAO,GAAG,gBAAgB,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;YACjC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,kBAAkB,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,8BAA8B;IAC9B,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC;QAC7C,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjD,IAAI,mBAAuC,CAAC;QAC5C,IAAI,gBAAoC,CAAC;QACzC,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;YAClC,mBAAmB,GAAG,QAAQ,CAAC;YAC/B,gBAAgB,GAAG,eAAe,CAAC;QACrC,CAAC;aAAM,CAAC;YACN,0EAA0E;YAC1E,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;gBACzB,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBACnC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;oBACtB,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC7B,gBAAgB,GAAG,GAAG,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9C,MAAM,YAAY,GAAG,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,mBAAmB,KAAK,SAAS,CAAC;QAChD,MAAM,cAAc,GAClB,CAAC,KAAK,IAAI,YAAY,GAAG,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,iBAAiB,GAAG,KAAK;YAC7B,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;YAC9B,CAAC,CAAC,QAAQ;iBACL,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC,CAAC;iBACjD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAE3B,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ;YACR,UAAU,EAAE,cAAc,CAAC,QAAQ,EAAE,YAAY,CAAC;YAClD,MAAM,EAAE,QAAQ,KAAK,YAAY,CAAC,yBAAyB;YAC3D,KAAK;YACL,cAAc;YACd,iBAAiB;YACjB,QAAQ;SACT,CAAC,CAAC;IACL,CAAC;IAED,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACrB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;YAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,UAAU,IAAI,EAAE,CAAC;QAClE,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,UAAU,IAAI,EAAE,CAAC;QAClE,OAAO,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,YAAY;IACZ,MAAM,aAAa,GAAG,IAAI,GAAG,EAAgD,CAAC;IAC9E,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;YACpC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IACD,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAgD,CAAC;IACjF,KAAK,MAAM,CAAC,IAAI,SAAS,CAAC,cAAc,EAAE,CAAC;QACzC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,KAAK,GAA0B,EAAE,CAAC;IACxC,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,MAAM,OAAO,GAA4B,EAAE,CAAC;IAE5C,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1E,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,CAAC;gBACP,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,eAAe,EAAE,IAAI,CAAC,UAAU;aACjC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,aAAa,EAAE,CAAC;QACtC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAED,WAAW;IACX,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAClC,IAAI,SAAS,IAAI,SAAS,CAAC,iBAAiB,KAAK,YAAY,CAAC,IAAI,EAAE,CAAC;QACnE,QAAQ,CAAC,OAAO,GAAG;YACjB,IAAI,EAAE,SAAS,CAAC,iBAAiB;YACjC,EAAE,EAAE,YAAY,CAAC,IAAI;SACtB,CAAC;IACJ,CAAC;IACD,yEAAyE;IACzE,yEAAyE;IAEzE,MAAM,SAAS,GAAG,SAAS,KAAK,SAAS,CAAC;IAC1C,MAAM,WAAW,GACf,CAAC,SAAS;QACV,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC;QACpD,KAAK,CAAC,MAAM,KAAK,CAAC;QAClB,OAAO,CAAC,MAAM,KAAK,CAAC;QACpB,OAAO,CAAC,MAAM,KAAK,CAAC;QACpB,CAAC,QAAQ,CAAC,OAAO;QACjB,CAAC,QAAQ,CAAC,YAAY,CAAC;IAEzB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC;AAChG,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { ClaudeSyncClient } from "../client/client.js";
2
+ import type { ConversationSummary } from "../models/types.js";
3
+ import { type SyncState } from "./state.js";
4
+ export type ExportFormat = "git" | "files" | "json";
5
+ export interface SyncConversationOptions {
6
+ format: ExportFormat;
7
+ authorName: string;
8
+ authorEmail: string;
9
+ /** Skip download entirely if list metadata matches stored state. */
10
+ skipSame?: boolean;
11
+ /** Skip if outputPath already exists (irrespective of state). */
12
+ skipExisting?: boolean;
13
+ /** Don't fetch artifacts. */
14
+ skipArtifacts?: boolean;
15
+ }
16
+ export interface SyncConversationResult {
17
+ action: "skipped" | "skipped-existing" | "full" | "incremental";
18
+ reason?: string;
19
+ changelogWritten: boolean;
20
+ }
21
+ /**
22
+ * Cheap predicate for --skip-same. Returns true when the list-endpoint summary
23
+ * matches what the sidecar state file recorded on the previous sync. Caller
24
+ * should still write a state file even when this returns false (bootstrap).
25
+ */
26
+ export declare function isSameByListMetadata(summary: Pick<ConversationSummary, "updated_at" | "current_leaf_message_uuid">, prevState: SyncState | undefined): boolean;
27
+ /**
28
+ * Orchestrates the sync of a single conversation: decides skip / full /
29
+ * incremental, fetches data, runs the right exporter, writes the state file
30
+ * and changelog. Returns metadata describing what happened.
31
+ *
32
+ * outputPath should be the conversation's directory (for files/git) or the
33
+ * directory that will hold `<slug>.json` (for json mode).
34
+ */
35
+ export declare function syncConversation(client: ClaudeSyncClient, orgId: string, summary: ConversationSummary, outputPath: string, options: SyncConversationOptions): Promise<SyncConversationResult>;
36
+ //# sourceMappingURL=incremental.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental.d.ts","sourceRoot":"","sources":["../../src/sync/incremental.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAS5B,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6BAA6B;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,SAAS,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAC;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,YAAY,GAAG,2BAA2B,CAAC,EAC9E,SAAS,EAAE,SAAS,GAAG,SAAS,GAC/B,OAAO,CAMT;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,mBAAmB,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CA+GjC"}
@@ -0,0 +1,229 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { rmSync } from "node:fs";
4
+ import { buildGitBundle } from "../export/bundle-builder.js";
5
+ import { exportToGit, appendToGit } from "../export/git-exporter.js";
6
+ import { diffConversation } from "./diff.js";
7
+ import { appendChangelog, renderChangelogSection, CHANGELOG_FILENAME, } from "./changelog.js";
8
+ import { readSyncState, writeSyncState, STATE_FILENAME, } from "./state.js";
9
+ import { buildMessageTree, findLeafMessages } from "../tree/message-tree.js";
10
+ /**
11
+ * Cheap predicate for --skip-same. Returns true when the list-endpoint summary
12
+ * matches what the sidecar state file recorded on the previous sync. Caller
13
+ * should still write a state file even when this returns false (bootstrap).
14
+ */
15
+ export function isSameByListMetadata(summary, prevState) {
16
+ if (!prevState)
17
+ return false;
18
+ if (prevState.updated_at !== summary.updated_at)
19
+ return false;
20
+ const prevLeaf = prevState.current_leaf_message_uuid ?? null;
21
+ const newLeaf = summary.current_leaf_message_uuid ?? null;
22
+ return prevLeaf === newLeaf;
23
+ }
24
+ /**
25
+ * Orchestrates the sync of a single conversation: decides skip / full /
26
+ * incremental, fetches data, runs the right exporter, writes the state file
27
+ * and changelog. Returns metadata describing what happened.
28
+ *
29
+ * outputPath should be the conversation's directory (for files/git) or the
30
+ * directory that will hold `<slug>.json` (for json mode).
31
+ */
32
+ export async function syncConversation(client, orgId, summary, outputPath, options) {
33
+ const stateDir = options.format === "json"
34
+ ? path.dirname(outputPath)
35
+ : outputPath;
36
+ // --skip-existing: legacy, dumb existence check.
37
+ if (options.skipExisting) {
38
+ const target = options.format === "json" ? outputPath + ".json" : outputPath;
39
+ if (fs.existsSync(target)) {
40
+ return {
41
+ action: "skipped-existing",
42
+ reason: "output exists",
43
+ changelogWritten: false,
44
+ };
45
+ }
46
+ }
47
+ // --skip-same: read prior state, compare list metadata.
48
+ let prevState;
49
+ if (fs.existsSync(stateDir)) {
50
+ try {
51
+ prevState = readSyncState(stateDir);
52
+ }
53
+ catch {
54
+ // Corrupted state -> fall through to full sync, will overwrite.
55
+ prevState = undefined;
56
+ }
57
+ }
58
+ if (options.skipSame && isSameByListMetadata(summary, prevState)) {
59
+ return {
60
+ action: "skipped",
61
+ reason: "unchanged since last sync",
62
+ changelogWritten: false,
63
+ };
64
+ }
65
+ // Fetch full tree + artifacts.
66
+ const conversation = await client.getConversation(orgId, summary.uuid, {
67
+ tree: true,
68
+ });
69
+ const { artifacts, artifactContents } = await fetchArtifacts(client, orgId, summary.uuid, !options.skipArtifacts);
70
+ const diff = diffConversation(prevState, conversation, artifacts);
71
+ // For json mode: bundle is the full snapshot, written as a single JSON file.
72
+ if (options.format === "json") {
73
+ const bundle = buildGitBundle(conversation, artifacts, artifactContents, {
74
+ authorName: options.authorName,
75
+ authorEmail: options.authorEmail,
76
+ multiBranch: true,
77
+ });
78
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
79
+ fs.writeFileSync(outputPath + ".json", JSON.stringify(bundle, null, 2), "utf-8");
80
+ // For json mode, write state file to the directory containing the json file.
81
+ writeStateFile(stateDir, summary, conversation, artifacts, prevState ? "incremental" : "full");
82
+ return {
83
+ action: prevState ? "incremental" : "full",
84
+ changelogWritten: false,
85
+ };
86
+ }
87
+ // For files / git mode: build a multi-branch bundle.
88
+ const bundle = buildGitBundle(conversation, artifacts, artifactContents, {
89
+ authorName: options.authorName,
90
+ authorEmail: options.authorEmail,
91
+ multiBranch: true,
92
+ });
93
+ const isFresh = !fs.existsSync(outputPath);
94
+ if (options.format === "git") {
95
+ if (isFresh) {
96
+ await exportToGit(bundle, outputPath);
97
+ }
98
+ else {
99
+ await appendToGit(bundle, outputPath);
100
+ }
101
+ }
102
+ else {
103
+ // files mode: stage in tmp, swap.
104
+ await writeFilesMode(bundle, outputPath);
105
+ }
106
+ // After successful swap: write CHANGELOG, .gitignore (git only), state file.
107
+ let changelogWritten = false;
108
+ const section = renderChangelogSection(diff, new Date());
109
+ if (section) {
110
+ appendChangelog(outputPath, section);
111
+ changelogWritten = true;
112
+ }
113
+ if (options.format === "git") {
114
+ ensureGitignore(outputPath);
115
+ }
116
+ writeStateFile(outputPath, summary, conversation, artifacts, prevState ? "incremental" : "full");
117
+ return {
118
+ action: prevState ? "incremental" : "full",
119
+ changelogWritten,
120
+ };
121
+ }
122
+ async function fetchArtifacts(client, orgId, convId, enabled) {
123
+ const empty = {
124
+ success: true,
125
+ files: [],
126
+ files_metadata: [],
127
+ };
128
+ if (!enabled)
129
+ return { artifacts: empty, artifactContents: new Map() };
130
+ let artifacts = empty;
131
+ const artifactContents = new Map();
132
+ try {
133
+ artifacts = await client.listArtifacts(orgId, convId);
134
+ for (const meta of artifacts.files_metadata) {
135
+ try {
136
+ const content = await client.downloadArtifact(orgId, convId, meta.path);
137
+ artifactContents.set(meta.path, content);
138
+ }
139
+ catch {
140
+ // Skip failed artifact downloads.
141
+ }
142
+ }
143
+ }
144
+ catch {
145
+ // Some conversations don't support artifacts.
146
+ }
147
+ return { artifacts, artifactContents };
148
+ }
149
+ /**
150
+ * Files mode: replay bundle into outputPath via the same tmp+swap pattern as
151
+ * exportToGit, but strip .git at the end.
152
+ */
153
+ async function writeFilesMode(bundle, outputPath) {
154
+ // Re-use exportToGit then strip .git. We need fresh tmp each time since
155
+ // exportToGit refuses to write into an existing path.
156
+ const stash = outputPath + ".prev";
157
+ const isUpdate = fs.existsSync(outputPath);
158
+ if (isUpdate) {
159
+ if (fs.existsSync(stash)) {
160
+ fs.rmSync(stash, { recursive: true, force: true });
161
+ }
162
+ fs.renameSync(outputPath, stash);
163
+ }
164
+ try {
165
+ await exportToGit(bundle, outputPath);
166
+ rmSync(path.join(outputPath, ".git"), { recursive: true, force: true });
167
+ // Preserve CHANGELOG.md from the previous tree (we'll append to it after).
168
+ if (isUpdate && fs.existsSync(path.join(stash, CHANGELOG_FILENAME))) {
169
+ fs.copyFileSync(path.join(stash, CHANGELOG_FILENAME), path.join(outputPath, CHANGELOG_FILENAME));
170
+ }
171
+ if (isUpdate && fs.existsSync(path.join(stash, STATE_FILENAME))) {
172
+ // State file gets rewritten by caller; nothing to preserve.
173
+ }
174
+ if (isUpdate) {
175
+ fs.rmSync(stash, { recursive: true, force: true });
176
+ }
177
+ }
178
+ catch (error) {
179
+ // Restore original on failure.
180
+ if (isUpdate) {
181
+ if (fs.existsSync(outputPath)) {
182
+ fs.rmSync(outputPath, { recursive: true, force: true });
183
+ }
184
+ if (fs.existsSync(stash)) {
185
+ fs.renameSync(stash, outputPath);
186
+ }
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ function ensureGitignore(repoDir) {
192
+ const gitignorePath = path.join(repoDir, ".gitignore");
193
+ const line = STATE_FILENAME;
194
+ let contents = "";
195
+ if (fs.existsSync(gitignorePath)) {
196
+ contents = fs.readFileSync(gitignorePath, "utf-8");
197
+ if (contents.split(/\r?\n/).some((l) => l.trim() === line)) {
198
+ return;
199
+ }
200
+ if (!contents.endsWith("\n"))
201
+ contents += "\n";
202
+ }
203
+ contents += `${line}\n${STATE_FILENAME}.tmp\n`;
204
+ fs.writeFileSync(gitignorePath, contents, "utf-8");
205
+ }
206
+ function writeStateFile(dir, summary, conversation, artifacts, action) {
207
+ const nodeMap = buildMessageTree(conversation.chat_messages);
208
+ const leaves = findLeafMessages(nodeMap).map((m) => ({
209
+ uuid: m.uuid,
210
+ last_message_index: m.index,
211
+ }));
212
+ const state = {
213
+ schema_version: 1,
214
+ conversation_uuid: conversation.uuid,
215
+ conversation_name: conversation.name,
216
+ updated_at: summary.updated_at,
217
+ current_leaf_message_uuid: conversation.current_leaf_message_uuid ?? null,
218
+ leaves,
219
+ artifacts: artifacts.files_metadata.map((a) => ({
220
+ path: a.path,
221
+ size: a.size,
222
+ created_at: a.created_at,
223
+ })),
224
+ last_sync_at: new Date().toISOString(),
225
+ last_sync_action: action,
226
+ };
227
+ writeSyncState(dir, state);
228
+ }
229
+ //# sourceMappingURL=incremental.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental.js","sourceRoot":"","sources":["../../src/sync/incremental.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
2
+ export declare const STATE_FILENAME = ".claudesync-state.json";
3
+ export declare const SyncStateLeafSchema: z.ZodObject<{
4
+ uuid: z.ZodString;
5
+ last_message_index: z.ZodNumber;
6
+ }, "strip", z.ZodTypeAny, {
7
+ uuid: string;
8
+ last_message_index: number;
9
+ }, {
10
+ uuid: string;
11
+ last_message_index: number;
12
+ }>;
13
+ export declare const SyncStateArtifactSchema: z.ZodObject<{
14
+ path: z.ZodString;
15
+ size: z.ZodNumber;
16
+ created_at: z.ZodString;
17
+ }, "strip", z.ZodTypeAny, {
18
+ path: string;
19
+ created_at: string;
20
+ size: number;
21
+ }, {
22
+ path: string;
23
+ created_at: string;
24
+ size: number;
25
+ }>;
26
+ export declare const SyncStateSchema: z.ZodObject<{
27
+ schema_version: z.ZodLiteral<1>;
28
+ conversation_uuid: z.ZodString;
29
+ conversation_name: z.ZodString;
30
+ updated_at: z.ZodString;
31
+ current_leaf_message_uuid: z.ZodNullable<z.ZodString>;
32
+ leaves: z.ZodArray<z.ZodObject<{
33
+ uuid: z.ZodString;
34
+ last_message_index: z.ZodNumber;
35
+ }, "strip", z.ZodTypeAny, {
36
+ uuid: string;
37
+ last_message_index: number;
38
+ }, {
39
+ uuid: string;
40
+ last_message_index: number;
41
+ }>, "many">;
42
+ artifacts: z.ZodArray<z.ZodObject<{
43
+ path: z.ZodString;
44
+ size: z.ZodNumber;
45
+ created_at: z.ZodString;
46
+ }, "strip", z.ZodTypeAny, {
47
+ path: string;
48
+ created_at: string;
49
+ size: number;
50
+ }, {
51
+ path: string;
52
+ created_at: string;
53
+ size: number;
54
+ }>, "many">;
55
+ last_sync_at: z.ZodString;
56
+ last_sync_action: z.ZodEnum<["full", "incremental", "skipped"]>;
57
+ }, "strip", z.ZodTypeAny, {
58
+ updated_at: string;
59
+ current_leaf_message_uuid: string | null;
60
+ conversation_uuid: string;
61
+ schema_version: 1;
62
+ conversation_name: string;
63
+ leaves: {
64
+ uuid: string;
65
+ last_message_index: number;
66
+ }[];
67
+ artifacts: {
68
+ path: string;
69
+ created_at: string;
70
+ size: number;
71
+ }[];
72
+ last_sync_at: string;
73
+ last_sync_action: "full" | "incremental" | "skipped";
74
+ }, {
75
+ updated_at: string;
76
+ current_leaf_message_uuid: string | null;
77
+ conversation_uuid: string;
78
+ schema_version: 1;
79
+ conversation_name: string;
80
+ leaves: {
81
+ uuid: string;
82
+ last_message_index: number;
83
+ }[];
84
+ artifacts: {
85
+ path: string;
86
+ created_at: string;
87
+ size: number;
88
+ }[];
89
+ last_sync_at: string;
90
+ last_sync_action: "full" | "incremental" | "skipped";
91
+ }>;
92
+ export type SyncState = z.infer<typeof SyncStateSchema>;
93
+ export type SyncStateLeaf = z.infer<typeof SyncStateLeafSchema>;
94
+ export type SyncStateArtifact = z.infer<typeof SyncStateArtifactSchema>;
95
+ /**
96
+ * Reads the sync state file from a conversation directory.
97
+ * Returns undefined if the file does not exist (bootstrap case).
98
+ * Throws on parse failure (corrupted state should not silently fall back to
99
+ * full re-sync without the user noticing).
100
+ */
101
+ export declare function readSyncState(dir: string): SyncState | undefined;
102
+ /**
103
+ * Writes the sync state file atomically (write to .tmp, then rename).
104
+ * Survives interruption: if the process dies mid-write, the original file
105
+ * (if any) is left intact.
106
+ */
107
+ export declare function writeSyncState(dir: string, state: SyncState): void;
108
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/sync/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,cAAc,2BAA2B,CAAC;AAEvD,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;EAIlC,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU1B,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAQhE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAMlE"}
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export const STATE_FILENAME = ".claudesync-state.json";
5
+ export const SyncStateLeafSchema = z.object({
6
+ uuid: z.string(),
7
+ last_message_index: z.number(),
8
+ });
9
+ export const SyncStateArtifactSchema = z.object({
10
+ path: z.string(),
11
+ size: z.number(),
12
+ created_at: z.string(),
13
+ });
14
+ export const SyncStateSchema = z.object({
15
+ schema_version: z.literal(1),
16
+ conversation_uuid: z.string(),
17
+ conversation_name: z.string(),
18
+ updated_at: z.string(),
19
+ current_leaf_message_uuid: z.string().nullable(),
20
+ leaves: z.array(SyncStateLeafSchema),
21
+ artifacts: z.array(SyncStateArtifactSchema),
22
+ last_sync_at: z.string(),
23
+ last_sync_action: z.enum(["full", "incremental", "skipped"]),
24
+ });
25
+ /**
26
+ * Reads the sync state file from a conversation directory.
27
+ * Returns undefined if the file does not exist (bootstrap case).
28
+ * Throws on parse failure (corrupted state should not silently fall back to
29
+ * full re-sync without the user noticing).
30
+ */
31
+ export function readSyncState(dir) {
32
+ const filePath = path.join(dir, STATE_FILENAME);
33
+ if (!fs.existsSync(filePath)) {
34
+ return undefined;
35
+ }
36
+ const raw = fs.readFileSync(filePath, "utf-8");
37
+ const parsed = JSON.parse(raw);
38
+ return SyncStateSchema.parse(parsed);
39
+ }
40
+ /**
41
+ * Writes the sync state file atomically (write to .tmp, then rename).
42
+ * Survives interruption: if the process dies mid-write, the original file
43
+ * (if any) is left intact.
44
+ */
45
+ export function writeSyncState(dir, state) {
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ const filePath = path.join(dir, STATE_FILENAME);
48
+ const tmpPath = filePath + ".tmp";
49
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
50
+ fs.renameSync(tmpPath, filePath);
51
+ }
52
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/sync/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEvD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE;CAC/B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;CACvB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5B,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC7B,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC7B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,yBAAyB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC;IACpC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,uBAAuB,CAAC;IAC3C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,gBAAgB,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;CAC7D,CAAC,CAAC;AAMH;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAChD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,KAAgB;IAC1D,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClC,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;IAC1E,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { buildMessageTree, findLeafMessages, getLinearBranch, } from "../message-tree.js";
2
+ import { buildMessageTree, findLeafMessages, getLinearBranch, getAllBranches, findDivergencePoint, shortLeafLabel, } from "../message-tree.js";
3
3
  /**
4
4
  * Helper to create a minimal ChatMessage for testing.
5
5
  * Only uuid, parent_message_uuid, index, and sender are meaningful
@@ -173,4 +173,64 @@ describe("getLinearBranch", () => {
173
173
  expect(branch.map((m) => m.uuid)).toEqual(["root", "a1"]);
174
174
  });
175
175
  });
176
+ describe("getAllBranches", () => {
177
+ it("returns one branch per leaf in a forked tree", () => {
178
+ const messages = [
179
+ makeMessage("root", "sentinel", 0, "human"),
180
+ makeMessage("a1", "root", 1, "assistant"),
181
+ makeMessage("h2-v1", "a1", 2, "human"),
182
+ makeMessage("h2-v2", "a1", 3, "human"),
183
+ makeMessage("a3-v2", "h2-v2", 4, "assistant"),
184
+ ];
185
+ const tree = buildMessageTree(messages);
186
+ const branches = getAllBranches(tree);
187
+ expect(branches.size).toBe(2);
188
+ expect(branches.has("h2-v1")).toBe(true);
189
+ expect(branches.has("a3-v2")).toBe(true);
190
+ expect(branches.get("h2-v1").map((m) => m.uuid)).toEqual([
191
+ "root",
192
+ "a1",
193
+ "h2-v1",
194
+ ]);
195
+ expect(branches.get("a3-v2").map((m) => m.uuid)).toEqual([
196
+ "root",
197
+ "a1",
198
+ "h2-v2",
199
+ "a3-v2",
200
+ ]);
201
+ });
202
+ });
203
+ describe("findDivergencePoint", () => {
204
+ it("finds the deepest shared ancestor between two branches", () => {
205
+ const messages = [
206
+ makeMessage("root", "sentinel", 0, "human"),
207
+ makeMessage("a1", "root", 1, "assistant"),
208
+ makeMessage("h2-v1", "a1", 2, "human"),
209
+ makeMessage("h2-v2", "a1", 3, "human"),
210
+ makeMessage("a3-v2", "h2-v2", 4, "assistant"),
211
+ ];
212
+ const tree = buildMessageTree(messages);
213
+ const v1 = getLinearBranch(tree, "h2-v1");
214
+ const v2 = getLinearBranch(tree, "a3-v2");
215
+ expect(findDivergencePoint(v1, v2)).toBe("a1");
216
+ });
217
+ it("returns undefined when branches share no ancestor", () => {
218
+ const m1 = makeMessage("a", "sentinel", 0);
219
+ const m2 = makeMessage("b", "sentinel", 1);
220
+ expect(findDivergencePoint([m1], [m2])).toBeUndefined();
221
+ });
222
+ });
223
+ describe("shortLeafLabel", () => {
224
+ it("returns 8-char prefix when unique", () => {
225
+ expect(shortLeafLabel("019ddeab-d142", ["019ddea7-ef2a"])).toBe("019ddeab");
226
+ });
227
+ it("falls back to 12 chars when 8 chars collide", () => {
228
+ expect(shortLeafLabel("019ddeab-d142-7b87-8a6a", [
229
+ "019ddeab-7716-7019-93f1",
230
+ ])).toBe("019ddeab-d14");
231
+ });
232
+ it("throws when no unique prefix exists within 16 chars", () => {
233
+ expect(() => shortLeafLabel("aaaa-bbbb-cccc-dddd", ["aaaa-bbbb-cccc-dddd-shared"])).toThrow();
234
+ });
235
+ });
176
236
  //# sourceMappingURL=message-tree.test.js.map