chainlesschain 0.162.12 → 0.162.14

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 (170) hide show
  1. package/README.md +29 -24
  2. package/package.json +5 -2
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-C3TDNq29.js → AIOps-D34d_Nh1.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-C9fE18pE.js → ActionButton-Br7HxCnl.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-wnZF602C.js → Analytics-bVKq79Xd.js} +1 -1
  7. package/src/assets/web-panel/assets/{AppLayout-BjgTMK7O.js → AppLayout-CWSLIbAz.js} +2 -2
  8. package/src/assets/web-panel/assets/{Audit-BBL0BW5_.js → Audit-Cmnu1qqa.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BKLqYCWU.js → Backup-Rok20-TL.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BGSzMCZs.js → BaseInput-BJzs_ZtT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CQWzZWEY.js → Chat-CSYapbcq.js} +1 -1
  12. package/src/assets/web-panel/assets/{Checkbox-BkTri12Q.js → Checkbox-BEa7Sr7e.js} +1 -1
  13. package/src/assets/web-panel/assets/{Codegen-BH1m09EO.js → Codegen-C9M4e7ne.js} +1 -1
  14. package/src/assets/web-panel/assets/{Col-BXnBuqIa.js → Col-DU9NoUIi.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-C_Nr4XCx.js → Community-DA9uz_jP.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-Du6GwLrj.js → Compact-3_bEraVw.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compliance-66M0oi1Q.js → Compliance-BtF8jWUQ.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cowork-DQrkZRNd.js → Cowork-BqvA7oaM.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cron-CwdIFH_v.js → Cron-CxZy7Mzg.js} +1 -1
  20. package/src/assets/web-panel/assets/{Crosschain-DqlcrQ9L.js → Crosschain-1DB-XRGu.js} +1 -1
  21. package/src/assets/web-panel/assets/{DID-OmPLKf7L.js → DID-B6Ezp1pt.js} +1 -1
  22. package/src/assets/web-panel/assets/{Dashboard-D_dampTL.js → Dashboard-QDJ6VVsn.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-CA1W7jAn.js → Dropdown-CovsWjxG.js} +1 -1
  24. package/src/assets/web-panel/assets/{Federation-Chlk9a7s.js → Federation-DbRxS4Y4.js} +1 -1
  25. package/src/assets/web-panel/assets/{FormItemContext-t0UqYFLq.js → FormItemContext-9E9dNGtx.js} +1 -1
  26. package/src/assets/web-panel/assets/{Git-CEq0raYm.js → Git-CqEpyxRZ.js} +2 -2
  27. package/src/assets/web-panel/assets/{Governance-C06CX7Ge.js → Governance-On47KtGq.js} +1 -1
  28. package/src/assets/web-panel/assets/{Inference-6VIFHxIP.js → Inference-RZcjcyaq.js} +1 -1
  29. package/src/assets/web-panel/assets/{KnowledgeGraph-BCJPjMBQ.js → KnowledgeGraph-C-1rRAM9.js} +1 -1
  30. package/src/assets/web-panel/assets/{Logs-BBpOYFct.js → Logs-BuunmG_r.js} +1 -1
  31. package/src/assets/web-panel/assets/{Marketplace-BFH6jMWt.js → Marketplace-CromymyA.js} +1 -1
  32. package/src/assets/web-panel/assets/{McpTools-uCFvRqGs.js → McpTools-5XlFExh7.js} +1 -1
  33. package/src/assets/web-panel/assets/{Memory-B0Kux_KT.js → Memory-DjnUT7YM.js} +1 -1
  34. package/src/assets/web-panel/assets/{MobileBridge-DHow2jiK.js → MobileBridge-BrYIgLg6.js} +1 -1
  35. package/src/assets/web-panel/assets/{MobileProjects-BFo9YQZp.js → MobileProjects-CL5V3fTm.js} +1 -1
  36. package/src/assets/web-panel/assets/{Mtc-riOh1G_F.js → Mtc-CHYJq6zK.js} +1 -1
  37. package/src/assets/web-panel/assets/{MtcAudit-Bm-hE2SP.js → MtcAudit-BZxUO0qt.js} +1 -1
  38. package/src/assets/web-panel/assets/{Multisig-DfUQxh5a.js → Multisig-FZTmJgW1.js} +2 -2
  39. package/src/assets/web-panel/assets/{NLProgramming-DuNvLBEq.js → NLProgramming-C9Mhefph.js} +1 -1
  40. package/src/assets/web-panel/assets/{Notes-DB20wd3c.js → Notes-W7usj-Ar.js} +1 -1
  41. package/src/assets/web-panel/assets/{NotificationSettings-CB-GkOWR.js → NotificationSettings-PBuYv_Bh.js} +1 -1
  42. package/src/assets/web-panel/assets/{Organization-3bU7PZuG.js → Organization-CuYCE-rF.js} +4 -4
  43. package/src/assets/web-panel/assets/{Overflow-BGCPP_0Y.js → Overflow-Dojx-kzE.js} +1 -1
  44. package/src/assets/web-panel/assets/{OverrideContext-x9ZzjLwk.js → OverrideContext-C_4H9tGA.js} +1 -1
  45. package/src/assets/web-panel/assets/{P2P-BHgAe1oC.js → P2P-BgIaSrLX.js} +1 -1
  46. package/src/assets/web-panel/assets/{Permissions-BuOD4xwc.js → Permissions-Byj2dkF_.js} +1 -1
  47. package/src/assets/web-panel/assets/PersonalDataHub-CMOOI13-.js +1 -0
  48. package/src/assets/web-panel/assets/PersonalDataHub-Dvaa8niQ.css +1 -0
  49. package/src/assets/web-panel/assets/{Pipeline-DBS5U4LB.js → Pipeline-CWwEOF09.js} +1 -1
  50. package/src/assets/web-panel/assets/{Privacy-UNjIc5El.js → Privacy-VT7gldcN.js} +1 -1
  51. package/src/assets/web-panel/assets/{ProjectInit-CicqCJGy.js → ProjectInit-7UH3c3p7.js} +1 -1
  52. package/src/assets/web-panel/assets/{ProjectSettings-CIxAbt4Y.js → ProjectSettings-DqLp-72a.js} +1 -1
  53. package/src/assets/web-panel/assets/{Projects-BJycZScO.js → Projects-B_54eDhH.js} +1 -1
  54. package/src/assets/web-panel/assets/{Providers-DxXvprme.js → Providers-BIrNfNpc.js} +1 -1
  55. package/src/assets/web-panel/assets/{QuickAsk-rrqjU8_Y.js → QuickAsk-BbYPwCso.js} +1 -1
  56. package/src/assets/web-panel/assets/{Recommend-BEwHMhI7.js → Recommend-BF4qBssF.js} +1 -1
  57. package/src/assets/web-panel/assets/{Reputation-DoVKCCMn.js → Reputation-DPEzlC2V.js} +1 -1
  58. package/src/assets/web-panel/assets/{Row-F5XcDhHr.js → Row-DjHxhH1L.js} +1 -1
  59. package/src/assets/web-panel/assets/{RssFeed-cZrRG7k8.js → RssFeed-D0_j678P.js} +1 -1
  60. package/src/assets/web-panel/assets/{Search-B9ctZjqx.js → Search-DctfGehu.js} +1 -1
  61. package/src/assets/web-panel/assets/{Security-Z62hl1mc.js → Security-BFHggeYM.js} +1 -1
  62. package/src/assets/web-panel/assets/{Services-CQf5XqgZ.js → Services-CmrFMukV.js} +1 -1
  63. package/src/assets/web-panel/assets/{Skeleton-DuCKw2Eh.js → Skeleton-DR4vn_nS.js} +1 -1
  64. package/src/assets/web-panel/assets/{Skills-qVkhva0s.js → Skills-DlXG2yyV.js} +1 -1
  65. package/src/assets/web-panel/assets/{Sla-BQbatr7s.js → Sla-4PPGL3SE.js} +1 -1
  66. package/src/assets/web-panel/assets/{SpeechSettings-DLFBzAgD.js → SpeechSettings-D9EhJOqm.js} +1 -1
  67. package/src/assets/web-panel/assets/{SyncSettings-CrzETZMW.js → SyncSettings-Dasmbi0p.js} +1 -1
  68. package/src/assets/web-panel/assets/{Tasks-D_EQ1nJ7.js → Tasks-vilEiuPA.js} +1 -1
  69. package/src/assets/web-panel/assets/{Templates-D4y-dGRc.js → Templates-Ca9Rvktn.js} +1 -1
  70. package/src/assets/web-panel/assets/{Tenant-2XI0jkPn.js → Tenant-CEZb9gfK.js} +1 -1
  71. package/src/assets/web-panel/assets/{Terminal-fUi5V2Z9.js → Terminal-DanCBdbD.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tokens-BuUNB2mg.js → Tokens-SPkClW2d.js} +1 -1
  73. package/src/assets/web-panel/assets/{Trigger-7DqLLuej.js → Trigger-B645yL7g.js} +1 -1
  74. package/src/assets/web-panel/assets/{Trust-CeACvTYx.js → Trust-D9sM_Ig0.js} +1 -1
  75. package/src/assets/web-panel/assets/{UkeySign-mDP9EXHq.js → UkeySign-B_Nr2K-u.js} +1 -1
  76. package/src/assets/web-panel/assets/{VideoEditing-veWlKclv.js → VideoEditing-U01Lea8j.js} +1 -1
  77. package/src/assets/web-panel/assets/{Wallet-Cd2Hheb8.js → Wallet-6xBySVV8.js} +1 -1
  78. package/src/assets/web-panel/assets/{WebAuthn-DyL7ZiHX.js → WebAuthn-DbgMoBu6.js} +3 -3
  79. package/src/assets/web-panel/assets/{WorkflowEditor-C7-7LJH9.js → WorkflowEditor-Bz-Y6IR2.js} +1 -1
  80. package/src/assets/web-panel/assets/{chat-DXomZMuo.js → chat-BC_O9hag.js} +1 -1
  81. package/src/assets/web-panel/assets/{collapseMotion-CjFH_Jop.js → collapseMotion-DfnRZex1.js} +1 -1
  82. package/src/assets/web-panel/assets/{colors-DlU92QNs.js → colors-ChlOGOvr.js} +1 -1
  83. package/src/assets/web-panel/assets/{compact-item-sBiTL8mX.js → compact-item-BSbAYGGF.js} +1 -1
  84. package/src/assets/web-panel/assets/{createContext-DZXEnzum.js → createContext-CFcZly5M.js} +1 -1
  85. package/src/assets/web-panel/assets/{echarts-Bq-n0MtJ.js → echarts-Dj_pBaVI.js} +1 -1
  86. package/src/assets/web-panel/assets/{hasIn-CpCHBZ2M.js → hasIn-BomYwwYE.js} +1 -1
  87. package/src/assets/web-panel/assets/{icons-CLQTHa5-.js → icons-BOPtEWK4.js} +4 -4
  88. package/src/assets/web-panel/assets/{index-B0Qbxr57.js → index-5Ewm6KZA.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-CjXSvceY.js → index-B6LJHQoE.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-CD3iljXs.js → index-BEJ6YiLI.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BK2AFy44.js → index-BGUbtM3R.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-Di1_EQ-X.js → index-BHGsFwYW.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-B23tuoo9.js → index-BHi69MHF.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DUlPMzoM.js → index-BI1jAWcc.js} +1 -1
  95. package/src/assets/web-panel/assets/index-BIRYt1of.js +1 -0
  96. package/src/assets/web-panel/assets/{index-B3k9UPHc.js → index-BYDvb1pi.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CwbWZubA.js → index-BZGdjNLA.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-ThrAiEF9.js → index-BwFykZ5U.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CUe5t5Aa.js → index-ByZQNO0A.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Dn8m1d1f.js → index-C0rr1X9W.js} +2 -2
  101. package/src/assets/web-panel/assets/{index-C6SDf50u.js → index-CBhoZhCO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-ClN_JuFa.js → index-CCRSz2cR.js} +1 -1
  103. package/src/assets/web-panel/assets/index-CZfySmWX.js +1 -0
  104. package/src/assets/web-panel/assets/{index-Dq5Rn5VS.js → index-Cj47XwJQ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-ChahjdYE.js → index-Cmzh8gKL.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Dyg6ikIL.js → index-CnxlKTDK.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Dj9Nvz6S.js → index-CoF95pYK.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-TwQZkVGh.js → index-Ctx97mH-.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-s8tvk-fF.js → index-D0vX9jQA.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-X48zYgZ6.js → index-DNkth8dM.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DygNvCeR.js → index-DRp5_Xns.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D_MzScPM.js → index-DW-Ji07y.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BqnhEJls.js → index-DXgE2VW6.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CDyzZ8_O.js → index-D_0B3CiU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C59FgSkU.js → index-Dbf5YmDX.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-_zyXBoS7.js → index-DsNQ2hqI.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CJ7XYa5K.js → index-EY733h9z.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-ibFHnqHz.js → index-QD_n54XT.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CivbS-57.js → index-T5Y_9IPv.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-C52udT0_.js → index-b8GbH2Yi.js} +4 -4
  121. package/src/assets/web-panel/assets/{index-XI6772AD.js → index-gUACAWbM.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-F9cBucYf.js → index-onW325hZ.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DKe9jmKG.js → index-ozVPr1gj.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D_IgY63-.js → index-slYX2rCE.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B_-RETt0.js → index-t9u2bHpH.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DeGnHcp5.js → index-za1GUJBG.js} +1 -1
  127. package/src/assets/web-panel/assets/{initDefaultProps-DEi92ZnZ.js → initDefaultProps-DnadEaxu.js} +1 -1
  128. package/src/assets/web-panel/assets/{motion-BtYKzpOc.js → motion-CC_Na0Tl.js} +1 -1
  129. package/src/assets/web-panel/assets/{move-Cb3A1-v-.js → move-C2d9Mkk9.js} +1 -1
  130. package/src/assets/web-panel/assets/{omit-B6qPDdOf.js → omit-QvpKbF8p.js} +1 -1
  131. package/src/assets/web-panel/assets/{pickAttrs-DDyeQMUc.js → pickAttrs-Dm8r3X1_.js} +1 -1
  132. package/src/assets/web-panel/assets/{placementArrow-BPV6VO47.js → placementArrow-DaqaVfoX.js} +1 -1
  133. package/src/assets/web-panel/assets/{responsiveObserve-DJ1ra4dT.js → responsiveObserve-Iida9fIn.js} +1 -1
  134. package/src/assets/web-panel/assets/{slide-D6v8tHvB.js → slide-YqHexXQD.js} +1 -1
  135. package/src/assets/web-panel/assets/{statusUtils-DulKcQLZ.js → statusUtils-BGKLoeEt.js} +1 -1
  136. package/src/assets/web-panel/assets/{styleChecker-Bne7zwMt.js → styleChecker-aI-gsQO8.js} +1 -1
  137. package/src/assets/web-panel/assets/useFlexGapSupport-BiOsz4rc.js +1 -0
  138. package/src/assets/web-panel/assets/{useFs-CR-iLa4Z.js → useFs-CZy7Zo2X.js} +1 -1
  139. package/src/assets/web-panel/assets/{useMergedState-O7QXt4P5.js → useMergedState-WwedrFR0.js} +1 -1
  140. package/src/assets/web-panel/assets/{useRefs-0J6m8UWN.js → useRefs-Cdq8EWeF.js} +1 -1
  141. package/src/assets/web-panel/assets/{useState-CSzR8F8O.js → useState-DGS1NOyn.js} +1 -1
  142. package/src/assets/web-panel/assets/{vendor-M5lGV-wr.js → vendor-DhFY8mDK.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-yL9axxBy.js → vnode-B6WqjmE4.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-B-VCMXSD.js → zoom-DTeTrJ2z.js} +1 -1
  145. package/src/assets/web-panel/index.html +3 -3
  146. package/src/commands/__tests__/android.test.js +260 -0
  147. package/src/commands/__tests__/hub-aichat.test.js +277 -0
  148. package/src/commands/__tests__/hub-wechat.test.js +243 -0
  149. package/src/commands/android.js +284 -0
  150. package/src/commands/hub.js +457 -0
  151. package/src/commands/sync-providers.js +436 -0
  152. package/src/gateways/ws/personal-data-hub-protocol.js +88 -0
  153. package/src/index.js +6 -0
  154. package/src/lib/__tests__/personal-data-hub-aichat-wizard.test.js +209 -0
  155. package/src/lib/__tests__/sync-credentials.test.js +265 -0
  156. package/src/lib/__tests__/sync-engine-cli.test.js +293 -0
  157. package/src/lib/cc-android-bridge.js +162 -0
  158. package/src/lib/personal-data-hub-aichat-wizard.js +242 -0
  159. package/src/lib/personal-data-hub-wiring.js +258 -13
  160. package/src/lib/sync-cli-db.js +194 -0
  161. package/src/lib/sync-credentials.js +225 -0
  162. package/src/lib/sync-engine-cli.js +406 -0
  163. package/src/lib/sync-oss-client.js +273 -0
  164. package/src/lib/sync-webdav-client.js +194 -0
  165. package/src/lib/web-ui-server.js +2 -1
  166. package/src/assets/web-panel/assets/PersonalDataHub--WA-aZAJ.js +0 -1
  167. package/src/assets/web-panel/assets/PersonalDataHub-BK7I0Rsb.css +0 -1
  168. package/src/assets/web-panel/assets/index-CcRX6BlT.js +0 -1
  169. package/src/assets/web-panel/assets/index-z6h6tqP3.js +0 -1
  170. package/src/assets/web-panel/assets/useFlexGapSupport-C1miTomM.js +0 -1
@@ -0,0 +1,293 @@
1
+ /**
2
+ * sync-engine-cli 单元测试 — Phase 3c follow-up Phase 2.
3
+ *
4
+ * 用 sql.js 内存 SQLite 模拟 dbManager.run/all/get;fake client;验证
5
+ * runSync 完整管线(push / conflict / hard error / tombstone drain /
6
+ * cursor 推进)。
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll, beforeEach, vi } from "vitest";
10
+
11
+ import {
12
+ runSync,
13
+ generateFilename,
14
+ generateMarkdown,
15
+ getCursor,
16
+ ensureCursor,
17
+ listTombstones,
18
+ } from "../sync-engine-cli.js";
19
+
20
+ class SqlJsDbManager {
21
+ constructor(db) {
22
+ this.db = db;
23
+ }
24
+ run(sql, params = []) {
25
+ this.db.run(sql, params);
26
+ }
27
+ get(sql, params = []) {
28
+ const stmt = this.db.prepare(sql);
29
+ stmt.bind(params);
30
+ let row;
31
+ if (stmt.step()) row = stmt.getAsObject();
32
+ stmt.free();
33
+ return row;
34
+ }
35
+ all(sql, params = []) {
36
+ const stmt = this.db.prepare(sql);
37
+ stmt.bind(params);
38
+ const rows = [];
39
+ while (stmt.step()) rows.push(stmt.getAsObject());
40
+ stmt.free();
41
+ return rows;
42
+ }
43
+ }
44
+
45
+ let SQL;
46
+ let dbManager;
47
+
48
+ beforeAll(async () => {
49
+ const initSqlJs = (await import("sql.js")).default;
50
+ SQL = await initSqlJs();
51
+ });
52
+
53
+ beforeEach(() => {
54
+ const db = new SQL.Database();
55
+ db.exec(`
56
+ CREATE TABLE knowledge_items (
57
+ id TEXT PRIMARY KEY,
58
+ title TEXT NOT NULL,
59
+ type TEXT NOT NULL DEFAULT 'note',
60
+ content TEXT,
61
+ tags TEXT,
62
+ created_at INTEGER NOT NULL,
63
+ updated_at INTEGER NOT NULL
64
+ );
65
+ CREATE TABLE sync_external_provider_cursor (
66
+ provider_id TEXT NOT NULL,
67
+ account_key TEXT NOT NULL DEFAULT '',
68
+ last_sync_at INTEGER NOT NULL DEFAULT 0,
69
+ last_item_id TEXT,
70
+ remote_etag_map TEXT NOT NULL DEFAULT '{}',
71
+ remote_filename_map TEXT NOT NULL DEFAULT '{}',
72
+ last_run_status TEXT,
73
+ last_run_error TEXT,
74
+ last_run_duration_ms INTEGER,
75
+ items_pushed INTEGER NOT NULL DEFAULT 0,
76
+ items_skipped INTEGER NOT NULL DEFAULT 0,
77
+ items_deleted INTEGER NOT NULL DEFAULT 0,
78
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
79
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
80
+ PRIMARY KEY (provider_id, account_key)
81
+ );
82
+ CREATE TABLE sync_external_tombstones (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ provider_id TEXT NOT NULL,
85
+ account_key TEXT NOT NULL DEFAULT '',
86
+ item_id TEXT NOT NULL,
87
+ resource_type TEXT,
88
+ deleted_at INTEGER NOT NULL,
89
+ retry_count INTEGER NOT NULL DEFAULT 0,
90
+ last_error TEXT,
91
+ UNIQUE(provider_id, account_key, item_id)
92
+ );
93
+ CREATE TRIGGER trg_sync_ext_tombstone_on_delete
94
+ AFTER DELETE ON knowledge_items
95
+ FOR EACH ROW
96
+ BEGIN
97
+ INSERT OR IGNORE INTO sync_external_tombstones
98
+ (provider_id, account_key, item_id, resource_type, deleted_at)
99
+ SELECT c.provider_id, c.account_key, OLD.id, 'KNOWLEDGE_ITEM',
100
+ (strftime('%s','now') * 1000)
101
+ FROM sync_external_provider_cursor c;
102
+ END;
103
+ `);
104
+ dbManager = new SqlJsDbManager(db);
105
+ });
106
+
107
+ function seedItems(items) {
108
+ for (const it of items) {
109
+ dbManager.run(
110
+ `INSERT INTO knowledge_items (id, title, type, content, created_at, updated_at)
111
+ VALUES (?, ?, ?, ?, ?, ?)`,
112
+ [
113
+ it.id,
114
+ it.title,
115
+ it.type ?? "note",
116
+ it.content ?? "body",
117
+ it.created_at ?? 1,
118
+ it.updated_at ?? 100,
119
+ ],
120
+ );
121
+ }
122
+ }
123
+
124
+ function makeFakeClient(overrides = {}) {
125
+ return {
126
+ putFile: vi.fn(
127
+ overrides.putFile ?? (async () => ({ ok: true, etag: "new-etag" })),
128
+ ),
129
+ deleteFile: vi.fn(overrides.deleteFile ?? (async () => ({ ok: true }))),
130
+ };
131
+ }
132
+
133
+ const baseDeps = () => ({ dbManager, providerId: "webdav", accountKey: "" });
134
+
135
+ describe("sync-engine-cli · renderer helpers", () => {
136
+ it("generateFilename cleans unsafe chars + collapses runs", () => {
137
+ const fn = generateFilename({ id: "abc", title: "My / cool: note?" });
138
+ expect(fn).toBe("abc-My_cool_note.md");
139
+ });
140
+
141
+ it("generateMarkdown emits front-matter", () => {
142
+ const md = generateMarkdown({
143
+ id: "x",
144
+ title: "T",
145
+ type: "note",
146
+ content: "body",
147
+ created_at: 1,
148
+ updated_at: 2,
149
+ tags: "a,b",
150
+ });
151
+ expect(md).toContain("---");
152
+ expect(md).toContain('title: "T"');
153
+ expect(md).toContain("tags: a,b");
154
+ expect(md.endsWith("body\n")).toBe(true);
155
+ });
156
+ });
157
+
158
+ describe("sync-engine-cli · ensureCursor + getCursor", () => {
159
+ it("ensureCursor creates row + returns it", () => {
160
+ const cursor = ensureCursor(dbManager, "webdav");
161
+ expect(cursor).toBeDefined();
162
+ expect(cursor.providerId).toBe("webdav");
163
+ expect(cursor.itemsPushed).toBe(0);
164
+ });
165
+
166
+ it("getCursor returns undefined when missing", () => {
167
+ expect(getCursor(dbManager, "oss")).toBeUndefined();
168
+ });
169
+ });
170
+
171
+ describe("sync-engine-cli · runSync happy path", () => {
172
+ it("pushes all items + advances cursor", async () => {
173
+ seedItems([
174
+ { id: "a", title: "A", updated_at: 100 },
175
+ { id: "b", title: "B", updated_at: 200 },
176
+ ]);
177
+ const client = makeFakeClient();
178
+ const res = await runSync({ ...baseDeps(), client });
179
+ expect(res.success).toBe(true);
180
+ expect(res.status).toBe("success");
181
+ expect(res.pushed).toBe(2);
182
+ expect(client.putFile).toHaveBeenCalledTimes(2);
183
+ const cursor = getCursor(dbManager, "webdav");
184
+ expect(cursor.lastItemId).toBe("b");
185
+ expect(cursor.itemsPushed).toBe(2);
186
+ });
187
+
188
+ it("re-run from advanced cursor pushes nothing new", async () => {
189
+ seedItems([{ id: "a", title: "A", updated_at: 100 }]);
190
+ const client = makeFakeClient();
191
+ await runSync({ ...baseDeps(), client });
192
+ client.putFile.mockClear();
193
+ const r2 = await runSync({ ...baseDeps(), client });
194
+ expect(r2.pushed).toBe(0);
195
+ expect(client.putFile).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("sends If-Match etag on modified item", async () => {
199
+ seedItems([{ id: "a", title: "A", updated_at: 100 }]);
200
+ const client = makeFakeClient();
201
+ await runSync({ ...baseDeps(), client });
202
+ dbManager.run(`UPDATE knowledge_items SET updated_at = 999 WHERE id = 'a'`);
203
+ client.putFile.mockClear();
204
+ await runSync({ ...baseDeps(), client });
205
+ expect(client.putFile).toHaveBeenCalledWith(
206
+ "a-A.md",
207
+ expect.any(String),
208
+ "new-etag",
209
+ );
210
+ });
211
+ });
212
+
213
+ describe("sync-engine-cli · conflict path (412)", () => {
214
+ it("412 → skipped + cursor advances + status=conflict", async () => {
215
+ seedItems([
216
+ { id: "a", title: "A", updated_at: 100 },
217
+ { id: "b", title: "B", updated_at: 200 },
218
+ ]);
219
+ const client = makeFakeClient({
220
+ putFile: vi.fn(async (fn) =>
221
+ fn === "a-A.md"
222
+ ? { ok: false, conflict: true, status: 412 }
223
+ : { ok: true, etag: "e" },
224
+ ),
225
+ });
226
+ const res = await runSync({ ...baseDeps(), client });
227
+ expect(res.status).toBe("conflict");
228
+ expect(res.pushed).toBe(1);
229
+ expect(res.skipped).toBe(1);
230
+ expect(getCursor(dbManager, "webdav").lastItemId).toBe("b");
231
+ });
232
+ });
233
+
234
+ describe("sync-engine-cli · hard error stops loop", () => {
235
+ it("4xx non-conflict → no advance past failure point", async () => {
236
+ seedItems([
237
+ { id: "a", title: "A", updated_at: 100 },
238
+ { id: "b", title: "B", updated_at: 200 },
239
+ ]);
240
+ const client = makeFakeClient({
241
+ putFile: vi.fn(async (fn) =>
242
+ fn === "b-B.md"
243
+ ? { ok: false, status: 403, error: "forbidden" }
244
+ : { ok: true, etag: "e" },
245
+ ),
246
+ });
247
+ const res = await runSync({ ...baseDeps(), client });
248
+ expect(res.success).toBe(false);
249
+ expect(res.status).toBe("failed");
250
+ expect(res.pushed).toBe(1);
251
+ expect(res.error).toMatch(/forbidden/);
252
+ expect(getCursor(dbManager, "webdav").lastItemId).toBe("a");
253
+ });
254
+ });
255
+
256
+ describe("sync-engine-cli · tombstone drain", () => {
257
+ it("drains tombstones via deleteFile + cleans maps", async () => {
258
+ seedItems([{ id: "a", title: "A", updated_at: 100 }]);
259
+ const client = makeFakeClient();
260
+ await runSync({ ...baseDeps(), client });
261
+ dbManager.run(`DELETE FROM knowledge_items WHERE id = 'a'`);
262
+ const res = await runSync({ ...baseDeps(), client });
263
+ expect(res.deleted).toBe(1);
264
+ expect(client.deleteFile).toHaveBeenCalledWith("a-A.md", "new-etag");
265
+ expect(listTombstones(dbManager, "webdav")).toHaveLength(0);
266
+ });
267
+ });
268
+
269
+ describe("sync-engine-cli · provider isolation", () => {
270
+ it("oss cursor independent of webdav cursor", async () => {
271
+ seedItems([{ id: "a", title: "A", updated_at: 100 }]);
272
+ const client = makeFakeClient();
273
+ await runSync({ dbManager, client, providerId: "webdav", accountKey: "" });
274
+ const webdav = getCursor(dbManager, "webdav");
275
+ const oss = getCursor(dbManager, "oss");
276
+ expect(webdav.itemsPushed).toBe(1);
277
+ expect(oss).toBeUndefined();
278
+ });
279
+ });
280
+
281
+ describe("sync-engine-cli · progress callback", () => {
282
+ it("emits start + final events", async () => {
283
+ seedItems([{ id: "a", title: "A", updated_at: 100 }]);
284
+ const events = [];
285
+ await runSync({
286
+ ...baseDeps(),
287
+ client: makeFakeClient(),
288
+ onProgress: (e) => events.push(e),
289
+ });
290
+ expect(events.find((e) => e.phase === "start")).toBeDefined();
291
+ expect(events[events.length - 1].phase).toBe("success");
292
+ });
293
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * cc-android-bridge — Node-side facade for the Android JNI bridge.
3
+ *
4
+ * Plan A Sub-Phase A6/A7 scaffold (see `docs/design/Personal_Data_Hub_Android_Standalone_Cc.md`
5
+ * §4.2 + §6.1). The eventual native binding (`cc-android-bridge.node`,
6
+ * loaded from `android-app/app/src/main/cpp/`) exposes Java/Kotlin APIs to
7
+ * the in-APK Node runtime: ContentResolver query, PackageManager listings,
8
+ * Runtime.exec / Shizuku / Magisk su, Accessibility node tree, SAF DocumentFile
9
+ * read.
10
+ *
11
+ * **v0.1 status**: the native .node binding is NOT yet shipped. This file is
12
+ * a pure stub — every `invoke()` call rejects with ANDROID_BRIDGE_NOT_AVAILABLE.
13
+ * It exists so the `cc android …` commands and `system-data-android` adapter
14
+ * (A7 / A8) can land + be unit-tested ahead of the JNI work. When A6 ships
15
+ * the .node binding, this file's `loadNativeBinding()` will resolve the real
16
+ * module; method dispatch is already wired.
17
+ *
18
+ * Detection precedence (highest first):
19
+ * 1. process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1" — test override; bridge
20
+ * reports "available" and routes through `_deps.testInvoke` (which tests
21
+ * replace with a mock). Lets us exercise the command code paths without
22
+ * a real device.
23
+ * 2. process.platform === "android"
24
+ * 3. process.env.PREFIX startsWith "/data/data/com.chainlesschain.android/"
25
+ * (Termux $PREFIX pattern in the bundled cc).
26
+ *
27
+ * Method names are kebab-case to match the `cc android <verb>` CLI surface,
28
+ * not the underlying JNI signatures (those use Java camelCase). Mapping
29
+ * happens in the future native binding.
30
+ */
31
+
32
+ import { createRequire } from "node:module";
33
+
34
+ const ANDROID_PREFIX = "/data/data/com.chainlesschain.android/";
35
+ const nodeRequire = createRequire(import.meta.url);
36
+
37
+ export function detectAndroid() {
38
+ if (process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1") return true;
39
+ if (process.platform === "android") return true;
40
+ if (
41
+ typeof process.env.PREFIX === "string" &&
42
+ process.env.PREFIX.startsWith(ANDROID_PREFIX)
43
+ ) {
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Lazy-load the native .node binding when we're actually on Android. Stays
51
+ * null elsewhere. The binding is expected at:
52
+ * $APK/lib/<abi>/cc-android-bridge.node
53
+ * resolved via require.resolve("cc-android-bridge").
54
+ *
55
+ * Wrapped in try so a missing binding (current state pre-A6) doesn't crash
56
+ * the CLI on import — `invoke()` reports it as a runtime error instead.
57
+ */
58
+ export function loadNativeBinding() {
59
+ if (!detectAndroid()) return null;
60
+ if (process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1") return null;
61
+ try {
62
+ return nodeRequire("cc-android-bridge");
63
+ } catch (_e) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export class AndroidBridgeUnavailableError extends Error {
69
+ constructor(reason) {
70
+ super(`ANDROID_BRIDGE_NOT_AVAILABLE: ${reason}`);
71
+ this.code = "ANDROID_BRIDGE_NOT_AVAILABLE";
72
+ this.reason = reason;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Invoke a bridge method.
78
+ *
79
+ * @param {string} method — kebab-case method name (e.g. "contacts.query",
80
+ * "fs.read", "a11y.query", "app.list").
81
+ * @param {object} [params]
82
+ * @returns {Promise<any>}
83
+ *
84
+ * Rejects with AndroidBridgeUnavailableError when:
85
+ * - Not running on Android (and CC_ANDROID_BRIDGE_OVERRIDE != 1)
86
+ * - Running on Android but native binding failed to load
87
+ * - Method is not registered in the native binding
88
+ */
89
+ export async function invoke(method, params = {}) {
90
+ if (typeof method !== "string" || method.length === 0) {
91
+ throw new TypeError(
92
+ "cc-android-bridge.invoke: method must be non-empty string",
93
+ );
94
+ }
95
+
96
+ // Test path — _deps.testInvoke replaced by harness.
97
+ if (
98
+ process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1" &&
99
+ typeof _deps.testInvoke === "function"
100
+ ) {
101
+ return await _deps.testInvoke(method, params);
102
+ }
103
+
104
+ if (!_deps.detectAndroid()) {
105
+ throw new AndroidBridgeUnavailableError(
106
+ `not running on Android (platform=${process.platform})`,
107
+ );
108
+ }
109
+
110
+ const native = _deps.loadNativeBinding();
111
+ if (!native) {
112
+ throw new AndroidBridgeUnavailableError(
113
+ "native binding cc-android-bridge.node missing — A6 JNI module not yet bundled",
114
+ );
115
+ }
116
+ if (typeof native.invoke !== "function") {
117
+ throw new AndroidBridgeUnavailableError(
118
+ "native binding loaded but does not export invoke()",
119
+ );
120
+ }
121
+ return await native.invoke(method, params);
122
+ }
123
+
124
+ /**
125
+ * Check whether the bridge is usable right now without throwing.
126
+ * Returns `{ available: boolean, reason?: string }`.
127
+ */
128
+ export function caps() {
129
+ if (process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1") {
130
+ return { available: true, reason: "test-override" };
131
+ }
132
+ if (!_deps.detectAndroid()) {
133
+ return {
134
+ available: false,
135
+ reason: `not-on-android (platform=${process.platform})`,
136
+ };
137
+ }
138
+ const native = _deps.loadNativeBinding();
139
+ if (!native) {
140
+ return {
141
+ available: false,
142
+ reason: "native-binding-missing (A6 pending)",
143
+ };
144
+ }
145
+ return { available: true };
146
+ }
147
+
148
+ // _deps injection seam (per [[feedback-vi-mock-cjs-interop]]). Tests reach in
149
+ // and replace these to exercise both success + error paths without needing
150
+ // a real Android device or the unloaded native binding.
151
+ export const _deps = {
152
+ detectAndroid,
153
+ loadNativeBinding,
154
+ testInvoke: null,
155
+ };
156
+
157
+ export default {
158
+ invoke,
159
+ caps,
160
+ AndroidBridgeUnavailableError,
161
+ _deps,
162
+ };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * AIChat WebView 鉴权向导 — cli / web-shell wiring (Phase 10.3.4).
3
+ *
4
+ * Mirrors `desktop-app-vue/src/main/personal-data-hub/aichat-wizard-factory.js`
5
+ * for the cli + web-shell side. Two structural differences from desktop:
6
+ *
7
+ * 1) `fallbackMode: "paste"` is hard-wired — cc ui does NOT have a
8
+ * BrowserView, so the wizard always returns the paste fallback shape.
9
+ *
10
+ * 2) Accounts store path lives under `getHub().hubDir`, same JSON file
11
+ * shape as desktop (`aichat-accounts.json`). Same hub directory on
12
+ * the same machine means desktop / cc ui share registered vendors
13
+ * (per the memo about WAL sharing — same caveat applies).
14
+ *
15
+ * Reference: docs/design/Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.md §2 §11
16
+ */
17
+
18
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { createRequire } from "node:module";
21
+
22
+ import {
23
+ DEFAULT_VENDOR_SPECS,
24
+ HttpClient,
25
+ CookieAuthSession,
26
+ } from "@chainlesschain/personal-data-hub/adapters/ai-chat-history";
27
+
28
+ const _require = createRequire(import.meta.url);
29
+
30
+ export const ACCOUNTS_FILE = "aichat-accounts.json";
31
+
32
+ /**
33
+ * JSON file accountsStore (same on-disk shape as desktop). Async API but
34
+ * implemented sync underneath — concurrent put() ops are chained.
35
+ */
36
+ export function createAccountsStore({ hubDir }) {
37
+ if (!hubDir || typeof hubDir !== "string") {
38
+ throw new Error("aichat-wizard-cli: hubDir required");
39
+ }
40
+ const filePath = join(hubDir, ACCOUNTS_FILE);
41
+ let writeChain = Promise.resolve();
42
+
43
+ function _readAll() {
44
+ try {
45
+ const raw = readFileSync(filePath, "utf-8");
46
+ const parsed = JSON.parse(raw);
47
+ return parsed && typeof parsed === "object" ? parsed : {};
48
+ } catch (err) {
49
+ if (err && err.code === "ENOENT") return {};
50
+ return {};
51
+ }
52
+ }
53
+
54
+ async function get(vendor) {
55
+ return _readAll()[vendor] || null;
56
+ }
57
+
58
+ async function put(vendor, entry) {
59
+ writeChain = writeChain.then(async () => {
60
+ const all = _readAll();
61
+ all[vendor] = entry;
62
+ try {
63
+ mkdirSync(hubDir, { recursive: true, mode: 0o700 });
64
+ } catch (err) {
65
+ if (!err || err.code !== "EEXIST") throw err;
66
+ }
67
+ writeFileSync(filePath, JSON.stringify(all, null, 2), { mode: 0o600 });
68
+ });
69
+ return writeChain;
70
+ }
71
+
72
+ async function del(vendor) {
73
+ writeChain = writeChain.then(async () => {
74
+ const all = _readAll();
75
+ if (!(vendor in all)) return;
76
+ delete all[vendor];
77
+ if (Object.keys(all).length === 0) {
78
+ try {
79
+ unlinkSync(filePath);
80
+ } catch (_e) {
81
+ /* missing is fine */
82
+ }
83
+ } else {
84
+ writeFileSync(filePath, JSON.stringify(all, null, 2), { mode: 0o600 });
85
+ }
86
+ });
87
+ return writeChain;
88
+ }
89
+
90
+ async function list() {
91
+ return Object.values(_readAll());
92
+ }
93
+
94
+ return { get, put, delete: del, list, _filePath: filePath };
95
+ }
96
+
97
+ /**
98
+ * Same bridge as desktop — translates wizard.registerVendor() into
99
+ * spec.validateCookie(). Kept duplicated so the cli build stays ESM-pure
100
+ * without reaching into desktop-app-vue.
101
+ */
102
+ export function createVendorAdapterBridge({
103
+ specs = DEFAULT_VENDOR_SPECS,
104
+ _httpClientFactory,
105
+ } = {}) {
106
+ // DEFAULT_VENDOR_SPECS is shipped as a vendor-keyed object; tests pass an
107
+ // array. Normalize to an array first so byVendor lookup works either way.
108
+ const arr = Array.isArray(specs)
109
+ ? specs
110
+ : specs && typeof specs === "object"
111
+ ? Object.values(specs)
112
+ : null;
113
+ if (!arr || arr.length === 0) {
114
+ throw new Error("aichat-wizard-cli: specs required");
115
+ }
116
+ const byVendor = new Map();
117
+ for (const s of arr) {
118
+ if (s && typeof s.name === "string") byVendor.set(s.name, s);
119
+ }
120
+ const buildClient =
121
+ _httpClientFactory ||
122
+ ((vendor, spec) => new HttpClient({ vendor, rateLimits: spec.rateLimits }));
123
+
124
+ async function registerVendor(vendor, cookies, _opts = {}) {
125
+ const spec = byVendor.get(vendor);
126
+ if (!spec) return { ok: false, reason: "UNKNOWN_VENDOR" };
127
+ if (typeof spec.validateCookie !== "function") {
128
+ return { ok: false, reason: "SPEC_MISSING_VALIDATE_COOKIE" };
129
+ }
130
+ let client;
131
+ try {
132
+ client = buildClient(vendor, spec);
133
+ } catch (err) {
134
+ return {
135
+ ok: false,
136
+ reason: "HTTP_CLIENT_INIT_FAILED",
137
+ error: err.message,
138
+ };
139
+ }
140
+ const session = new CookieAuthSession({
141
+ vendor,
142
+ cookies: _jarToArray(cookies),
143
+ });
144
+ try {
145
+ const r = await spec.validateCookie({ httpClient: client, session });
146
+ return r || { ok: false, reason: "VALIDATE_RETURNED_NULL" };
147
+ } catch (err) {
148
+ return { ok: false, reason: "VALIDATE_THREW", error: err.message };
149
+ }
150
+ }
151
+
152
+ return { registerVendor };
153
+ }
154
+
155
+ export function _jarToArray(input) {
156
+ if (Array.isArray(input)) {
157
+ return input.filter(
158
+ (c) => c && typeof c.name === "string" && typeof c.value === "string",
159
+ );
160
+ }
161
+ if (typeof input === "string") {
162
+ const out = [];
163
+ for (const pair of input.split(/;\s*/)) {
164
+ const idx = pair.indexOf("=");
165
+ if (idx <= 0) continue;
166
+ const name = pair.slice(0, idx).trim();
167
+ const value = pair.slice(idx + 1).trim();
168
+ if (name && value) out.push({ name, value });
169
+ }
170
+ return out;
171
+ }
172
+ if (input && typeof input === "object") {
173
+ const out = [];
174
+ for (const [name, value] of Object.entries(input)) {
175
+ if (typeof value === "string" && value.length > 0)
176
+ out.push({ name, value });
177
+ }
178
+ return out;
179
+ }
180
+ return [];
181
+ }
182
+
183
+ const _wizardsByHubDir = new Map();
184
+
185
+ /**
186
+ * cli-side wizard singleton. Always builds in `fallbackMode: "paste"` —
187
+ * cc ui never opens a BrowserView. Tests substitute `_accountsStore` and
188
+ * `_vendorAdapter` for hermetic runs.
189
+ */
190
+ export function getAIChatWizard({
191
+ hubDir,
192
+ _accountsStore,
193
+ _vendorAdapter,
194
+ _deps,
195
+ } = {}) {
196
+ if (!hubDir) throw new Error("aichat-wizard-cli: hubDir required");
197
+ const isTest = !!(_accountsStore || _vendorAdapter || _deps);
198
+ if (!isTest && _wizardsByHubDir.has(hubDir))
199
+ return _wizardsByHubDir.get(hubDir);
200
+
201
+ const accountsStore = _accountsStore || createAccountsStore({ hubDir });
202
+ const vendorAdapter = _vendorAdapter || createVendorAdapterBridge();
203
+ // Paste-mode is the default for cli. Tests can override via _deps.
204
+ const deps = _deps || {
205
+ sessionFactory: () => ({}),
206
+ clock: () => Date.now(),
207
+ logger: { info: () => {}, warn: () => {}, error: () => {} },
208
+ fallbackMode: "paste",
209
+ };
210
+ if (deps && !deps.fallbackMode) deps.fallbackMode = "paste";
211
+ // The controller resolves classifier / specLookup / knownVendors itself
212
+ // when _deps doesn't supply them — but since we ARE supplying _deps, we
213
+ // must fill those slots too. Re-import the spec module to avoid coupling
214
+ // tests to those exact defaults.
215
+ if (!deps.classifier || !deps.specLookup) {
216
+ // Lazy require to keep top-of-file clean.
217
+ const spec = _require(
218
+ "@chainlesschain/personal-data-hub/adapters/ai-chat-history/cookie-capture-spec",
219
+ );
220
+ deps.classifier = deps.classifier || spec.classifyProbedCookies;
221
+ deps.specLookup = deps.specLookup || spec.getSpec;
222
+ deps.knownVendors = deps.knownVendors || spec.listVendors();
223
+ deps.cookieSpecVersion = deps.cookieSpecVersion || spec.COOKIE_SPEC_VERSION;
224
+ }
225
+ // Lazy require (same pattern as cookie-capture-spec above) — keeps
226
+ // module-load tolerant of older @chainlesschain/personal-data-hub
227
+ // versions that don't yet export this subpath.
228
+ const { createAIChatWizardController } = _require(
229
+ "@chainlesschain/personal-data-hub/adapters/ai-chat-history/wizard-controller",
230
+ );
231
+ const wiz = createAIChatWizardController({
232
+ accountsStore,
233
+ vendorAdapter,
234
+ _deps: deps,
235
+ });
236
+ if (!isTest) _wizardsByHubDir.set(hubDir, wiz);
237
+ return wiz;
238
+ }
239
+
240
+ export function _resetForTests() {
241
+ _wizardsByHubDir.clear();
242
+ }