chainlesschain 0.162.13 → 0.162.15

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 (147) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/assets/{AIOps-Bq_zxhCr.js → AIOps-Dr5poTWh.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CaevDm9t.js → ActionButton-DLOGPHQ-.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-B0gOmwPw.js → Analytics-DBlO3FPX.js} +1 -1
  6. package/src/assets/web-panel/assets/{AppLayout-DWhZiV0Q.js → AppLayout-BrMarmWx.js} +2 -2
  7. package/src/assets/web-panel/assets/{Audit-ZuZJBCxo.js → Audit-DX6omFqA.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-CX7jhH5l.js → Backup-0QSLlQkB.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-CVLx7HVq.js → BaseInput-Dn4GFfR8.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-C6yL5tRD.js → Chat-CMTeA0Zo.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-BePhbqVq.js → Checkbox-BQQxf6iA.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-B5E4x1Lm.js → Codegen-D2vrazFC.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-CWhNU6A7.js → Col-P1hVDVKm.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-mSEAuJhp.js → Community-CPcDkABv.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DSudHzX3.js → Compact-DvBQwE-b.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-CxJLYjyn.js → Compliance-GX2qsHkq.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-C-trppQj.js → Cowork-CEEISrBb.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cron-_Ij4v5fY.js → Cron-Y3D6wB4C.js} +1 -1
  19. package/src/assets/web-panel/assets/{Crosschain-eLHXzp5T.js → Crosschain-CnCjDKKD.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-BP04AUUB.js → DID-CJ2EFB5b.js} +1 -1
  21. package/src/assets/web-panel/assets/{Dashboard-CzEeQv62.js → Dashboard-CWujrs70.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-C_SGOB22.js → Dropdown-BzKSQA5P.js} +1 -1
  23. package/src/assets/web-panel/assets/{Federation-BgaP4BOv.js → Federation-DgaAWIwd.js} +1 -1
  24. package/src/assets/web-panel/assets/{FormItemContext-BxGLLt9r.js → FormItemContext-BitgB4Gb.js} +1 -1
  25. package/src/assets/web-panel/assets/{Git-Bt_uM_Gw.js → Git-Bzj4vLLf.js} +1 -1
  26. package/src/assets/web-panel/assets/{Governance-N2-0RG_o.js → Governance-D7DEdSBD.js} +1 -1
  27. package/src/assets/web-panel/assets/{Inference-eS3g-CzP.js → Inference-CVsGLfcX.js} +1 -1
  28. package/src/assets/web-panel/assets/{KnowledgeGraph-CUuvNVah.js → KnowledgeGraph-CF6C6ZNo.js} +1 -1
  29. package/src/assets/web-panel/assets/{Logs-wPbwEt2r.js → Logs-C0Er-tt8.js} +1 -1
  30. package/src/assets/web-panel/assets/{Marketplace-DH91kTwo.js → Marketplace-_E7uwUUU.js} +1 -1
  31. package/src/assets/web-panel/assets/{McpTools-D-GyyBrI.js → McpTools-dHbktLMV.js} +1 -1
  32. package/src/assets/web-panel/assets/{Memory-BOtUy-tw.js → Memory-CaQ4UY7b.js} +1 -1
  33. package/src/assets/web-panel/assets/{MobileBridge-DIP__XQd.js → MobileBridge-BbOh0uJt.js} +1 -1
  34. package/src/assets/web-panel/assets/{MobileProjects-1nqr1UsU.js → MobileProjects-CfdNEimZ.js} +1 -1
  35. package/src/assets/web-panel/assets/{Mtc-CCE0x7h2.js → Mtc-HLvyepRP.js} +1 -1
  36. package/src/assets/web-panel/assets/{MtcAudit-BBkz0XUO.js → MtcAudit-DMaHGoIr.js} +1 -1
  37. package/src/assets/web-panel/assets/{Multisig-CIKSJvTY.js → Multisig-DFNZf0AB.js} +1 -1
  38. package/src/assets/web-panel/assets/{NLProgramming-BDkgeFcq.js → NLProgramming-cZwXhWy2.js} +1 -1
  39. package/src/assets/web-panel/assets/{Notes-ONiUxfN1.js → Notes-U_3n8Zid.js} +1 -1
  40. package/src/assets/web-panel/assets/{NotificationSettings-CGcyKEIe.js → NotificationSettings-B4VvhkKf.js} +1 -1
  41. package/src/assets/web-panel/assets/{Organization-BZtMYBJt.js → Organization-BIwBFrdX.js} +1 -1
  42. package/src/assets/web-panel/assets/{Overflow-C4LfFZAI.js → Overflow-_zxR8gF2.js} +1 -1
  43. package/src/assets/web-panel/assets/{P2P-D71Cpk-m.js → P2P-DJXmWhLC.js} +1 -1
  44. package/src/assets/web-panel/assets/{Permissions-DRGYF29v.js → Permissions-DwJ35K8Z.js} +1 -1
  45. package/src/assets/web-panel/assets/{PersonalDataHub-CfRYoIua.js → PersonalDataHub-CzZXLXGN.js} +1 -1
  46. package/src/assets/web-panel/assets/{Pipeline-BOyp0_Qo.js → Pipeline-DjtRGM8X.js} +1 -1
  47. package/src/assets/web-panel/assets/{Privacy-a_AcphvF.js → Privacy-BJhsIhGf.js} +1 -1
  48. package/src/assets/web-panel/assets/{ProjectInit-CFu1grYt.js → ProjectInit-DEx5SDLh.js} +1 -1
  49. package/src/assets/web-panel/assets/{ProjectSettings-p54Eivhh.js → ProjectSettings-B6qDg7nn.js} +1 -1
  50. package/src/assets/web-panel/assets/{Projects-DkB88XAu.js → Projects-CKkTgrBf.js} +1 -1
  51. package/src/assets/web-panel/assets/{Providers-EK7f8DEd.js → Providers-3GEshcW_.js} +1 -1
  52. package/src/assets/web-panel/assets/{QuickAsk-CpO0w3iP.js → QuickAsk-BfUXbKxa.js} +1 -1
  53. package/src/assets/web-panel/assets/{Recommend-BUJVQdv9.js → Recommend-DgxYasSJ.js} +1 -1
  54. package/src/assets/web-panel/assets/{Reputation-kU2fOuZt.js → Reputation-CWvVg_V1.js} +1 -1
  55. package/src/assets/web-panel/assets/{Row-oGWRbk6g.js → Row-42M5ot1v.js} +1 -1
  56. package/src/assets/web-panel/assets/{RssFeed-saC_46Yo.js → RssFeed-Do3isL1x.js} +1 -1
  57. package/src/assets/web-panel/assets/{Search-CjtRqFUu.js → Search-_7wgwjaR.js} +1 -1
  58. package/src/assets/web-panel/assets/{Security-BX0NBVfQ.js → Security-DKB6pe8t.js} +1 -1
  59. package/src/assets/web-panel/assets/{Services-Bgxsnei_.js → Services-BHrBJAcB.js} +1 -1
  60. package/src/assets/web-panel/assets/{Skeleton-CwBb3k2a.js → Skeleton-C8L-EfGp.js} +1 -1
  61. package/src/assets/web-panel/assets/{Skills-CZCMYH6Z.js → Skills-CU66huTh.js} +1 -1
  62. package/src/assets/web-panel/assets/{Sla-Djy1uHnZ.js → Sla-DA8AKNmI.js} +1 -1
  63. package/src/assets/web-panel/assets/{SpeechSettings-CGUI_Uyh.js → SpeechSettings-CufjYTzV.js} +1 -1
  64. package/src/assets/web-panel/assets/{SyncSettings-DK2CKHRD.js → SyncSettings-D_VAYHIg.js} +1 -1
  65. package/src/assets/web-panel/assets/{Tasks-BKiOzeIO.js → Tasks-BE6nYh9k.js} +1 -1
  66. package/src/assets/web-panel/assets/{Templates-CnQpleXj.js → Templates-BfRbttd6.js} +1 -1
  67. package/src/assets/web-panel/assets/{Tenant-DwKz0cjm.js → Tenant-YO71CL80.js} +1 -1
  68. package/src/assets/web-panel/assets/{Terminal-A7t_wsR8.js → Terminal-D1mQaE_Z.js} +1 -1
  69. package/src/assets/web-panel/assets/{Tokens-BqYY9l44.js → Tokens-CvnNQtX4.js} +1 -1
  70. package/src/assets/web-panel/assets/{Trigger-BI4bXFmi.js → Trigger-BnN19FJt.js} +1 -1
  71. package/src/assets/web-panel/assets/{Trust-yMynKTRG.js → Trust-C6oZLHqp.js} +1 -1
  72. package/src/assets/web-panel/assets/{UkeySign-Br4IScM6.js → UkeySign-BOhFYuWu.js} +1 -1
  73. package/src/assets/web-panel/assets/{VideoEditing-CWcThGsP.js → VideoEditing-NMc6-qc-.js} +1 -1
  74. package/src/assets/web-panel/assets/{Wallet-CZcAtjxj.js → Wallet-BALmcKtd.js} +1 -1
  75. package/src/assets/web-panel/assets/{WebAuthn-BnTZFMA0.js → WebAuthn-6IVe-W6O.js} +1 -1
  76. package/src/assets/web-panel/assets/{WorkflowEditor-N7gGz3_n.js → WorkflowEditor-BIv20DXb.js} +1 -1
  77. package/src/assets/web-panel/assets/{chat-D175ZIO0.js → chat-CiceyA69.js} +1 -1
  78. package/src/assets/web-panel/assets/{colors-LKhZyttv.js → colors-CxwvpRW0.js} +1 -1
  79. package/src/assets/web-panel/assets/{compact-item-CsJSebxT.js → compact-item-N6ASDseQ.js} +1 -1
  80. package/src/assets/web-panel/assets/{createContext-BJ_CPYFC.js → createContext-CQ8PEhyP.js} +1 -1
  81. package/src/assets/web-panel/assets/{hasIn-CzD3IqH8.js → hasIn-DDiIuryC.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-BhiZDGg7.js → index-1jBrqw2R.js} +1 -1
  83. package/src/assets/web-panel/assets/index-1losWCP0.js +1 -0
  84. package/src/assets/web-panel/assets/{index-CRGNuUIM.js → index-92sMXPmR.js} +1 -1
  85. package/src/assets/web-panel/assets/{index-Dm-3kvtD.js → index-BCgZTQf8.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-BZluCuTH.js → index-BKLatStI.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-CTIkCKav.js → index-BKmY57ry.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-E7t1hAnk.js → index-BZ4O1Vp7.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-D7ZcBI5s.js → index-BdmcwwMp.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BNVLVzN5.js → index-Bj-mAQFK.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-R1cFADfk.js → index-BjvirvV4.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-BvF2tC6C.js → index-Bw6UqIkJ.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-6qPbrYF7.js → index-BzdEj9_B.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BMvdoiFr.js → index-C43WIe_p.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BjOrt4vw.js → index-CHsNn-Qv.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BsirlkJ0.js → index-COIxnEwP.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D401L3yx.js → index-C_dbCu-F.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BDSZDDb2.js → index-Cb0QVAZL.js} +3 -3
  99. package/src/assets/web-panel/assets/{index-BV-__mlC.js → index-CcUK2M7W.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DXp1jVsK.js → index-ClASVywF.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-D8kltMTW.js → index-CnArbjXg.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Kn-Of5ew.js → index-Cur_KKpV.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-RIO4JKMP.js → index-Cwk0olXV.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CKjBAdm0.js → index-D0yG93O4.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BSNibAqz.js → index-D58a5SL3.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-uTEVWPYA.js → index-DEjAgx2R.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DJVkBmSc.js → index-DUKjS4kF.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BmJdof_c.js → index-DWKigrAM.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C2HBKw07.js → index-DcMESTJs.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DnPt5OdD.js → index-DgwpVvQq.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DOO73rHE.js → index-Dj2wgF3A.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D5IZCkZG.js → index-DobYLmfZ.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-JTX9A7w0.js → index-Dp3r80qO.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CyHdYUeZ.js → index-DtpWqJ-2.js} +1 -1
  115. package/src/assets/web-panel/assets/index-EoP_WtDt.js +1 -0
  116. package/src/assets/web-panel/assets/{index-BEDFHKO3.js → index-_BHtKVC_.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BXH9ujMW.js → index-bYorCa3r.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Dd7dICwB.js → index-hn9LVkIY.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B7UYymse.js → index-mmpauJ3E.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DM3uBEWD.js → index-xQlQRo2j.js} +1 -1
  121. package/src/assets/web-panel/assets/{initDefaultProps-CBW0okek.js → initDefaultProps-1MKZtqXZ.js} +1 -1
  122. package/src/assets/web-panel/assets/{motion-DGAffQ0Z.js → motion-DFBQpRS8.js} +1 -1
  123. package/src/assets/web-panel/assets/{move-DFJ0-5IW.js → move-CiI8Ada0.js} +1 -1
  124. package/src/assets/web-panel/assets/{omit-AvrDghg1.js → omit-CjeFoPcC.js} +1 -1
  125. package/src/assets/web-panel/assets/{pickAttrs-D7csw9i1.js → pickAttrs-Be3kefJq.js} +1 -1
  126. package/src/assets/web-panel/assets/{placementArrow-hZ6Lg6kG.js → placementArrow-Be5Ra1_B.js} +1 -1
  127. package/src/assets/web-panel/assets/{responsiveObserve-DLLx5VvS.js → responsiveObserve-NCu3YHiX.js} +1 -1
  128. package/src/assets/web-panel/assets/{slide-BaRIT3ev.js → slide-f23lSB4X.js} +1 -1
  129. package/src/assets/web-panel/assets/{statusUtils-Cdjyuhrz.js → statusUtils-Dq99US_U.js} +1 -1
  130. package/src/assets/web-panel/assets/{styleChecker-CbrNybTt.js → styleChecker-B-UUq5Ww.js} +1 -1
  131. package/src/assets/web-panel/assets/{useFlexGapSupport-B8gAhiRC.js → useFlexGapSupport-D6aUzeVO.js} +1 -1
  132. package/src/assets/web-panel/assets/{useFs-BZPy4ICP.js → useFs-DU-R5a4I.js} +1 -1
  133. package/src/assets/web-panel/assets/{vnode-6Y0NDMVv.js → vnode-N7r8LSGe.js} +1 -1
  134. package/src/assets/web-panel/assets/{zoom-DTbMGsSH.js → zoom-07xoxB1t.js} +1 -1
  135. package/src/assets/web-panel/index.html +1 -1
  136. package/src/commands/__tests__/android.test.js +260 -0
  137. package/src/commands/__tests__/hub-wechat.test.js +186 -15
  138. package/src/commands/android.js +284 -0
  139. package/src/commands/hub.js +263 -19
  140. package/src/gateways/ws/personal-data-hub-protocol.js +28 -8
  141. package/src/index.js +2 -0
  142. package/src/lib/__tests__/cc-android-bridge.test.js +245 -0
  143. package/src/lib/cc-android-bridge.js +206 -0
  144. package/src/lib/personal-data-hub-wiring.js +98 -20
  145. package/src/lib/web-ui-server.js +2 -1
  146. package/src/assets/web-panel/assets/index-CY1mQA2I.js +0 -1
  147. package/src/assets/web-panel/assets/index-D9nXHfUB.js +0 -1
@@ -24,6 +24,9 @@ import {
24
24
  import { getAIChatWizard } from "../../lib/personal-data-hub-aichat-wizard.js";
25
25
  import { existsSync, unlinkSync, readdirSync } from "node:fs";
26
26
  import { join } from "node:path";
27
+ import pdhPkg from "@chainlesschain/personal-data-hub";
28
+
29
+ const { ingestSystemDataAndroidSnapshot } = pdhPkg;
27
30
 
28
31
  async function withHub(fn) {
29
32
  try {
@@ -42,6 +45,22 @@ export const PERSONAL_DATA_HUB_HANDLERS = {
42
45
  return await hub.engine.ask(msg.question, msg.options || {});
43
46
  }),
44
47
 
48
+ // Path Y: prompt context only, no LLM call. Lets web-shell / mobile host
49
+ // its own inference (Volcengine Doubao, OpenRouter, etc.) while keeping
50
+ // vault retrieval centralized.
51
+ "personal-data-hub.retrieve-context": async (msg) =>
52
+ withHub(async (hub) => {
53
+ if (!hub.engine) throw new Error("Analysis engine unavailable");
54
+ return await hub.engine.retrieveContext(msg.question, msg.options || {});
55
+ }),
56
+
57
+ // Path C: phone-collected ContentResolver + PackageManager snapshot →
58
+ // staging file → syncAdapter(system-data-android). Returns SyncReport.
59
+ "personal-data-hub.ingest-system-data-android": async (msg) =>
60
+ withHub(
61
+ async (hub) => await ingestSystemDataAndroidSnapshot(hub, msg.snapshot),
62
+ ),
63
+
45
64
  "personal-data-hub.stats": async () =>
46
65
  withHub((hub) => ({
47
66
  vault: hub.vault.stats(),
@@ -219,14 +238,15 @@ export const PERSONAL_DATA_HUB_HANDLERS = {
219
238
  withHub(async (hub) => await hub.probeWechatEnv()),
220
239
 
221
240
  "personal-data-hub.register-wechat": async (msg) =>
222
- withHub(async (hub) =>
223
- await hub.registerWechatAdapter({
224
- account: msg.account,
225
- dbPath: msg.dbPath,
226
- wechatDataPath: msg.wechatDataPath,
227
- fridaOpts: msg.fridaOpts,
228
- keyProviderOverride: msg.keyProviderOverride,
229
- }),
241
+ withHub(
242
+ async (hub) =>
243
+ await hub.registerWechatAdapter({
244
+ account: msg.account,
245
+ dbPath: msg.dbPath,
246
+ wechatDataPath: msg.wechatDataPath,
247
+ fridaOpts: msg.fridaOpts,
248
+ keyProviderOverride: msg.keyProviderOverride,
249
+ }),
230
250
  ),
231
251
 
232
252
  "personal-data-hub.unregister-wechat": async (msg) =>
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ import { registerChatCommand } from "./commands/chat.js";
16
16
  import { registerAskCommand } from "./commands/ask.js";
17
17
  import { registerLlmCommand } from "./commands/llm.js";
18
18
  import { registerHubCommand } from "./commands/hub.js";
19
+ import { registerAndroidCommand } from "./commands/android.js";
19
20
  import {
20
21
  registerAgentCommand,
21
22
  registerSubAgentV2Command,
@@ -407,6 +408,7 @@ export function createProgram(opts = {}) {
407
408
  registerAskCommand(program);
408
409
  registerLlmCommand(program);
409
410
  registerHubCommand(program);
411
+ registerAndroidCommand(program);
410
412
  registerAgentCommand(program);
411
413
  registerSubAgentV2Command(program);
412
414
  registerExecBackendV2Command(program);
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
+ import bridgeDefault, {
5
+ invoke,
6
+ caps,
7
+ AndroidBridgeUnavailableError,
8
+ detectAndroid,
9
+ loadBridgeConfig,
10
+ _deps,
11
+ } from "../cc-android-bridge.js";
12
+
13
+ // ─── env restore helpers ──────────────────────────────────────────────
14
+
15
+ const ENV_KEYS = [
16
+ "CC_ANDROID_BRIDGE_OVERRIDE",
17
+ "CC_ANDROID_BRIDGE_CONFIG_DIR",
18
+ "PREFIX",
19
+ ];
20
+ let envSnapshot;
21
+ let depsSnapshot;
22
+
23
+ beforeEach(() => {
24
+ envSnapshot = {};
25
+ for (const k of ENV_KEYS) envSnapshot[k] = process.env[k];
26
+ depsSnapshot = {
27
+ detectAndroid: _deps.detectAndroid,
28
+ loadBridgeConfig: _deps.loadBridgeConfig,
29
+ fetch: _deps.fetch,
30
+ testInvoke: _deps.testInvoke,
31
+ };
32
+ });
33
+
34
+ afterEach(() => {
35
+ for (const k of ENV_KEYS) {
36
+ if (envSnapshot[k] === undefined) delete process.env[k];
37
+ else process.env[k] = envSnapshot[k];
38
+ }
39
+ _deps.detectAndroid = depsSnapshot.detectAndroid;
40
+ _deps.loadBridgeConfig = depsSnapshot.loadBridgeConfig;
41
+ _deps.fetch = depsSnapshot.fetch;
42
+ _deps.testInvoke = depsSnapshot.testInvoke;
43
+ });
44
+
45
+ // ─── detectAndroid ────────────────────────────────────────────────────
46
+
47
+ describe("detectAndroid", () => {
48
+ it("returns false on non-Android host", () => {
49
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
50
+ delete process.env.PREFIX;
51
+ // platform = "win32" / "linux" / "darwin" — none is "android"
52
+ expect(detectAndroid()).toBe(false);
53
+ });
54
+
55
+ it("returns true under CC_ANDROID_BRIDGE_OVERRIDE=1", () => {
56
+ process.env.CC_ANDROID_BRIDGE_OVERRIDE = "1";
57
+ expect(detectAndroid()).toBe(true);
58
+ });
59
+
60
+ it("returns true when PREFIX matches Termux Android prefix", () => {
61
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
62
+ process.env.PREFIX = "/data/data/com.chainlesschain.android/files/usr";
63
+ expect(detectAndroid()).toBe(true);
64
+ });
65
+ });
66
+
67
+ // ─── loadBridgeConfig ─────────────────────────────────────────────────
68
+
69
+ describe("loadBridgeConfig", () => {
70
+ it("returns null off-device with no PREFIX + no override dir", () => {
71
+ delete process.env.CC_ANDROID_BRIDGE_CONFIG_DIR;
72
+ delete process.env.PREFIX;
73
+ expect(loadBridgeConfig()).toBeNull();
74
+ });
75
+
76
+ it("returns parsed config from CC_ANDROID_BRIDGE_CONFIG_DIR", async () => {
77
+ const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
78
+ const { join } = await import("node:path");
79
+ const { tmpdir } = await import("node:os");
80
+
81
+ const dir = mkdtempSync(join(tmpdir(), "cc-bridge-cfg-"));
82
+ writeFileSync(join(dir, "port"), "12345\n", "utf-8");
83
+ writeFileSync(join(dir, "token"), "deadbeef\n", "utf-8");
84
+ process.env.CC_ANDROID_BRIDGE_CONFIG_DIR = dir;
85
+
86
+ const cfg = loadBridgeConfig();
87
+ expect(cfg).toEqual({
88
+ port: 12345,
89
+ token: "deadbeef",
90
+ baseUrl: "http://127.0.0.1:12345",
91
+ });
92
+
93
+ rmSync(dir, { recursive: true, force: true });
94
+ });
95
+
96
+ it("returns null when port file missing", async () => {
97
+ const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
98
+ const { join } = await import("node:path");
99
+ const { tmpdir } = await import("node:os");
100
+ const dir = mkdtempSync(join(tmpdir(), "cc-bridge-cfg-"));
101
+ writeFileSync(join(dir, "token"), "x", "utf-8");
102
+ process.env.CC_ANDROID_BRIDGE_CONFIG_DIR = dir;
103
+ expect(loadBridgeConfig()).toBeNull();
104
+ rmSync(dir, { recursive: true, force: true });
105
+ });
106
+ });
107
+
108
+ // ─── invoke ───────────────────────────────────────────────────────────
109
+
110
+ describe("invoke — error paths", () => {
111
+ it("throws TypeError on empty method", async () => {
112
+ await expect(invoke("")).rejects.toThrow(TypeError);
113
+ });
114
+
115
+ it("rejects ANDROID_BRIDGE_NOT_AVAILABLE off-device", async () => {
116
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
117
+ _deps.detectAndroid = () => false;
118
+ await expect(invoke("contacts.query")).rejects.toBeInstanceOf(
119
+ AndroidBridgeUnavailableError,
120
+ );
121
+ });
122
+
123
+ it("rejects when bridge config missing on-device", async () => {
124
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
125
+ _deps.detectAndroid = () => true;
126
+ _deps.loadBridgeConfig = () => null;
127
+ await expect(invoke("contacts.query")).rejects.toMatchObject({
128
+ code: "ANDROID_BRIDGE_NOT_AVAILABLE",
129
+ reason: expect.stringContaining("bridge config missing"),
130
+ });
131
+ });
132
+
133
+ it("rejects on fetch network error", async () => {
134
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
135
+ _deps.detectAndroid = () => true;
136
+ _deps.loadBridgeConfig = () => ({
137
+ port: 8237,
138
+ token: "x",
139
+ baseUrl: "http://127.0.0.1:8237",
140
+ });
141
+ _deps.fetch = vi.fn(async () => {
142
+ throw new Error("ECONNREFUSED");
143
+ });
144
+ await expect(invoke("contacts.query")).rejects.toMatchObject({
145
+ code: "ANDROID_BRIDGE_NOT_AVAILABLE",
146
+ reason: expect.stringContaining("ECONNREFUSED"),
147
+ });
148
+ });
149
+
150
+ it("rejects on non-2xx response", async () => {
151
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
152
+ _deps.detectAndroid = () => true;
153
+ _deps.loadBridgeConfig = () => ({
154
+ port: 8237,
155
+ token: "x",
156
+ baseUrl: "http://127.0.0.1:8237",
157
+ });
158
+ _deps.fetch = vi.fn(async () => ({
159
+ ok: false,
160
+ status: 401,
161
+ text: async () => "",
162
+ }));
163
+ await expect(invoke("contacts.query")).rejects.toMatchObject({
164
+ reason: expect.stringContaining("HTTP 401"),
165
+ });
166
+ });
167
+ });
168
+
169
+ describe("invoke — happy path", () => {
170
+ it("POSTs /invoke with Bearer token + JSON body, returns parsed JSON", async () => {
171
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
172
+ _deps.detectAndroid = () => true;
173
+ _deps.loadBridgeConfig = () => ({
174
+ port: 8237,
175
+ token: "secret-token",
176
+ baseUrl: "http://127.0.0.1:8237",
177
+ });
178
+ const fetchSpy = vi.fn(async (url, init) => ({
179
+ ok: true,
180
+ status: 200,
181
+ text: async () => JSON.stringify({ contacts: [{ name: "Alice" }] }),
182
+ }));
183
+ _deps.fetch = fetchSpy;
184
+
185
+ const result = await invoke("contacts.query", { since: 0 });
186
+ expect(result).toEqual({ contacts: [{ name: "Alice" }] });
187
+ expect(fetchSpy).toHaveBeenCalledOnce();
188
+ const [url, init] = fetchSpy.mock.calls[0];
189
+ expect(url).toBe("http://127.0.0.1:8237/invoke?method=contacts.query");
190
+ expect(init.method).toBe("POST");
191
+ expect(init.headers["Authorization"]).toBe("Bearer secret-token");
192
+ expect(init.headers["Content-Type"]).toBe("application/json");
193
+ expect(JSON.parse(init.body)).toEqual({ since: 0 });
194
+ });
195
+
196
+ it("uses testInvoke when CC_ANDROID_BRIDGE_OVERRIDE=1", async () => {
197
+ process.env.CC_ANDROID_BRIDGE_OVERRIDE = "1";
198
+ _deps.testInvoke = vi.fn(async (m, p) => ({ echo: m, ...p }));
199
+ const result = await invoke("foo.bar", { x: 1 });
200
+ expect(result).toEqual({ echo: "foo.bar", x: 1 });
201
+ });
202
+ });
203
+
204
+ // ─── caps ─────────────────────────────────────────────────────────────
205
+
206
+ describe("caps", () => {
207
+ it("returns available=false off-device", () => {
208
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
209
+ _deps.detectAndroid = () => false;
210
+ const r = caps();
211
+ expect(r.available).toBe(false);
212
+ expect(r.reason).toContain("not-on-android");
213
+ });
214
+
215
+ it("returns available=false when bridge server not started", () => {
216
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
217
+ _deps.detectAndroid = () => true;
218
+ _deps.loadBridgeConfig = () => null;
219
+ const r = caps();
220
+ expect(r.available).toBe(false);
221
+ expect(r.reason).toContain("bridge-server-not-started");
222
+ });
223
+
224
+ it("returns available=true with port when bridge is up", () => {
225
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
226
+ _deps.detectAndroid = () => true;
227
+ _deps.loadBridgeConfig = () => ({ port: 8237, token: "x" });
228
+ const r = caps();
229
+ expect(r.available).toBe(true);
230
+ expect(r.port).toBe(8237);
231
+ });
232
+ });
233
+
234
+ // ─── default export ───────────────────────────────────────────────────
235
+
236
+ describe("default export", () => {
237
+ it("exposes invoke / caps / Error class / _deps", () => {
238
+ expect(bridgeDefault.invoke).toBe(invoke);
239
+ expect(bridgeDefault.caps).toBe(caps);
240
+ expect(bridgeDefault.AndroidBridgeUnavailableError).toBe(
241
+ AndroidBridgeUnavailableError,
242
+ );
243
+ expect(bridgeDefault._deps).toBe(_deps);
244
+ });
245
+ });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * cc-android-bridge — Node-side facade for the Android JNI bridge.
3
+ *
4
+ * Plan A Sub-Phase A6 (see `docs/design/Personal_Data_Hub_Android_Standalone_Cc.md`).
5
+ *
6
+ * **A6a transport (current)**: HTTP localhost. The Android app starts a
7
+ * tiny HTTP server (CcAndroidBridgeServer.kt) at App.onCreate; this file
8
+ * reads the port + token written to filesDir/.chainlesschain/bridge/ and
9
+ * forwards `invoke(method, params)` as `POST /invoke?method=<kebab>` with
10
+ * the JSON params as body and `Authorization: Bearer <token>`.
11
+ *
12
+ * Why HTTP not JNI: the cc subprocess is a separate Linux process (per
13
+ * memory android_cc_subprocess_execve_via_mksh) — JNI cannot bridge
14
+ * process boundaries. HTTP localhost gives the same UX with zero NDK
15
+ * toolchain dependency. The .so + Unix-socket path stays on the roadmap
16
+ * (A6b) for the in-process feel but is not blocking the feature.
17
+ *
18
+ * Detection precedence (highest first):
19
+ * 1. process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1" — test override
20
+ * 2. process.platform === "android"
21
+ * 3. process.env.PREFIX startsWith "/data/data/com.chainlesschain.android/"
22
+ * (Termux $PREFIX pattern in the bundled cc).
23
+ *
24
+ * Bridge config discovery (Android only):
25
+ * - filesDir/.chainlesschain/bridge/port — "8237" etc.
26
+ * - filesDir/.chainlesschain/bridge/token — 48-hex-char auth token
27
+ * filesDir resolves to `$PREFIX/..` (Termux convention: filesDir is the
28
+ * parent of usr/).
29
+ *
30
+ * Method names are kebab-case to match the `cc android <verb>` CLI surface.
31
+ */
32
+
33
+ import { readFileSync, existsSync } from "node:fs";
34
+ import { join, dirname } from "node:path";
35
+
36
+ const ANDROID_PREFIX = "/data/data/com.chainlesschain.android/";
37
+
38
+ export function detectAndroid() {
39
+ if (process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1") return true;
40
+ if (process.platform === "android") return true;
41
+ if (
42
+ typeof process.env.PREFIX === "string" &&
43
+ process.env.PREFIX.startsWith(ANDROID_PREFIX)
44
+ ) {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Locate the Android app's filesDir from inside the in-APK cc subprocess.
52
+ * Termux convention: $PREFIX = filesDir/usr → filesDir = dirname($PREFIX).
53
+ * Falls back to env var CC_ANDROID_BRIDGE_CONFIG_DIR (LAN / desktop case
54
+ * where caller has discovered the Android filesDir some other way).
55
+ */
56
+ function resolveBridgeConfigDir() {
57
+ if (process.env.CC_ANDROID_BRIDGE_CONFIG_DIR) {
58
+ return process.env.CC_ANDROID_BRIDGE_CONFIG_DIR;
59
+ }
60
+ if (
61
+ typeof process.env.PREFIX === "string" &&
62
+ process.env.PREFIX.startsWith(ANDROID_PREFIX)
63
+ ) {
64
+ return join(dirname(process.env.PREFIX), ".chainlesschain", "bridge");
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Read the port + token written by CcAndroidBridgeServer.start() in
71
+ * filesDir/.chainlesschain/bridge/. Returns null if either file is missing
72
+ * (server not started yet, or running off-device without override).
73
+ */
74
+ export function loadBridgeConfig() {
75
+ const dir = resolveBridgeConfigDir();
76
+ if (!dir) return null;
77
+ try {
78
+ const portPath = join(dir, "port");
79
+ const tokenPath = join(dir, "token");
80
+ if (!existsSync(portPath) || !existsSync(tokenPath)) return null;
81
+ const port = parseInt(readFileSync(portPath, "utf-8").trim(), 10);
82
+ const token = readFileSync(tokenPath, "utf-8").trim();
83
+ if (!Number.isFinite(port) || port <= 0 || !token) return null;
84
+ return { port, token, baseUrl: `http://127.0.0.1:${port}` };
85
+ } catch (_e) {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ export class AndroidBridgeUnavailableError extends Error {
91
+ constructor(reason) {
92
+ super(`ANDROID_BRIDGE_NOT_AVAILABLE: ${reason}`);
93
+ this.code = "ANDROID_BRIDGE_NOT_AVAILABLE";
94
+ this.reason = reason;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Invoke a bridge method via the HTTP transport.
100
+ *
101
+ * @param {string} method — kebab-case method name (e.g. "contacts.query")
102
+ * @param {object} [params]
103
+ * @returns {Promise<any>} Parsed JSON response from the bridge.
104
+ *
105
+ * Rejects with AndroidBridgeUnavailableError when:
106
+ * - Not running on Android and CC_ANDROID_BRIDGE_OVERRIDE != 1
107
+ * - Running on Android but bridge config (port/token) missing
108
+ * - HTTP request fails or server returns non-200
109
+ * - Method name unknown (server returns {"error": "UNKNOWN_METHOD"})
110
+ */
111
+ export async function invoke(method, params = {}) {
112
+ if (typeof method !== "string" || method.length === 0) {
113
+ throw new TypeError(
114
+ "cc-android-bridge.invoke: method must be non-empty string",
115
+ );
116
+ }
117
+
118
+ // Test path — harness replaces _deps.testInvoke.
119
+ if (
120
+ process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1" &&
121
+ typeof _deps.testInvoke === "function"
122
+ ) {
123
+ return await _deps.testInvoke(method, params);
124
+ }
125
+
126
+ if (!_deps.detectAndroid()) {
127
+ throw new AndroidBridgeUnavailableError(
128
+ `not running on Android (platform=${process.platform})`,
129
+ );
130
+ }
131
+
132
+ const config = _deps.loadBridgeConfig();
133
+ if (!config) {
134
+ throw new AndroidBridgeUnavailableError(
135
+ "bridge config missing — CcAndroidBridgeServer not started? (filesDir/.chainlesschain/bridge/{port,token})",
136
+ );
137
+ }
138
+
139
+ const url = `${config.baseUrl}/invoke?method=${encodeURIComponent(method)}`;
140
+ let resp;
141
+ try {
142
+ resp = await _deps.fetch(url, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ Authorization: `Bearer ${config.token}`,
147
+ },
148
+ body: JSON.stringify(params || {}),
149
+ });
150
+ } catch (e) {
151
+ throw new AndroidBridgeUnavailableError(
152
+ `HTTP transport error: ${e && e.message ? e.message : String(e)}`,
153
+ );
154
+ }
155
+ if (!resp.ok) {
156
+ throw new AndroidBridgeUnavailableError(`bridge HTTP ${resp.status}`);
157
+ }
158
+ const text = await resp.text();
159
+ try {
160
+ return JSON.parse(text);
161
+ } catch (e) {
162
+ throw new AndroidBridgeUnavailableError(
163
+ `bridge returned non-JSON: ${text.slice(0, 200)}`,
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Check whether the bridge is reachable right now without throwing.
170
+ * Returns `{ available: boolean, reason?: string, port?: number }`.
171
+ */
172
+ export function caps() {
173
+ if (process.env.CC_ANDROID_BRIDGE_OVERRIDE === "1") {
174
+ return { available: true, reason: "test-override" };
175
+ }
176
+ if (!_deps.detectAndroid()) {
177
+ return {
178
+ available: false,
179
+ reason: `not-on-android (platform=${process.platform})`,
180
+ };
181
+ }
182
+ const config = _deps.loadBridgeConfig();
183
+ if (!config) {
184
+ return {
185
+ available: false,
186
+ reason: "bridge-server-not-started (port/token files missing)",
187
+ };
188
+ }
189
+ return { available: true, port: config.port };
190
+ }
191
+
192
+ // _deps injection seam (per [[feedback-vi-mock-cjs-interop]]). Tests reach in
193
+ // and replace these to exercise success / error paths without a real device.
194
+ export const _deps = {
195
+ detectAndroid,
196
+ loadBridgeConfig,
197
+ fetch: globalThis.fetch.bind(globalThis),
198
+ testInvoke: null,
199
+ };
200
+
201
+ export default {
202
+ invoke,
203
+ caps,
204
+ AndroidBridgeUnavailableError,
205
+ _deps,
206
+ };