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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-Bq_zxhCr.js → AIOps-Dr5poTWh.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CaevDm9t.js → ActionButton-DLOGPHQ-.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B0gOmwPw.js → Analytics-DBlO3FPX.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-DWhZiV0Q.js → AppLayout-BrMarmWx.js} +2 -2
- package/src/assets/web-panel/assets/{Audit-ZuZJBCxo.js → Audit-DX6omFqA.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CX7jhH5l.js → Backup-0QSLlQkB.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CVLx7HVq.js → BaseInput-Dn4GFfR8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C6yL5tRD.js → Chat-CMTeA0Zo.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BePhbqVq.js → Checkbox-BQQxf6iA.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B5E4x1Lm.js → Codegen-D2vrazFC.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CWhNU6A7.js → Col-P1hVDVKm.js} +1 -1
- package/src/assets/web-panel/assets/{Community-mSEAuJhp.js → Community-CPcDkABv.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DSudHzX3.js → Compact-DvBQwE-b.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CxJLYjyn.js → Compliance-GX2qsHkq.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-C-trppQj.js → Cowork-CEEISrBb.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-_Ij4v5fY.js → Cron-Y3D6wB4C.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-eLHXzp5T.js → Crosschain-CnCjDKKD.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BP04AUUB.js → DID-CJ2EFB5b.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-CzEeQv62.js → Dashboard-CWujrs70.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-C_SGOB22.js → Dropdown-BzKSQA5P.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BgaP4BOv.js → Federation-DgaAWIwd.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BxGLLt9r.js → FormItemContext-BitgB4Gb.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Bt_uM_Gw.js → Git-Bzj4vLLf.js} +1 -1
- package/src/assets/web-panel/assets/{Governance-N2-0RG_o.js → Governance-D7DEdSBD.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-eS3g-CzP.js → Inference-CVsGLfcX.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CUuvNVah.js → KnowledgeGraph-CF6C6ZNo.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-wPbwEt2r.js → Logs-C0Er-tt8.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DH91kTwo.js → Marketplace-_E7uwUUU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D-GyyBrI.js → McpTools-dHbktLMV.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-BOtUy-tw.js → Memory-CaQ4UY7b.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-DIP__XQd.js → MobileBridge-BbOh0uJt.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-1nqr1UsU.js → MobileProjects-CfdNEimZ.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CCE0x7h2.js → Mtc-HLvyepRP.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-BBkz0XUO.js → MtcAudit-DMaHGoIr.js} +1 -1
- package/src/assets/web-panel/assets/{Multisig-CIKSJvTY.js → Multisig-DFNZf0AB.js} +1 -1
- package/src/assets/web-panel/assets/{NLProgramming-BDkgeFcq.js → NLProgramming-cZwXhWy2.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-ONiUxfN1.js → Notes-U_3n8Zid.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-CGcyKEIe.js → NotificationSettings-B4VvhkKf.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BZtMYBJt.js → Organization-BIwBFrdX.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-C4LfFZAI.js → Overflow-_zxR8gF2.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D71Cpk-m.js → P2P-DJXmWhLC.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-DRGYF29v.js → Permissions-DwJ35K8Z.js} +1 -1
- package/src/assets/web-panel/assets/{PersonalDataHub-CfRYoIua.js → PersonalDataHub-CzZXLXGN.js} +1 -1
- package/src/assets/web-panel/assets/{Pipeline-BOyp0_Qo.js → Pipeline-DjtRGM8X.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-a_AcphvF.js → Privacy-BJhsIhGf.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CFu1grYt.js → ProjectInit-DEx5SDLh.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-p54Eivhh.js → ProjectSettings-B6qDg7nn.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-DkB88XAu.js → Projects-CKkTgrBf.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-EK7f8DEd.js → Providers-3GEshcW_.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CpO0w3iP.js → QuickAsk-BfUXbKxa.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BUJVQdv9.js → Recommend-DgxYasSJ.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-kU2fOuZt.js → Reputation-CWvVg_V1.js} +1 -1
- package/src/assets/web-panel/assets/{Row-oGWRbk6g.js → Row-42M5ot1v.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-saC_46Yo.js → RssFeed-Do3isL1x.js} +1 -1
- package/src/assets/web-panel/assets/{Search-CjtRqFUu.js → Search-_7wgwjaR.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BX0NBVfQ.js → Security-DKB6pe8t.js} +1 -1
- package/src/assets/web-panel/assets/{Services-Bgxsnei_.js → Services-BHrBJAcB.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-CwBb3k2a.js → Skeleton-C8L-EfGp.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CZCMYH6Z.js → Skills-CU66huTh.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Djy1uHnZ.js → Sla-DA8AKNmI.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CGUI_Uyh.js → SpeechSettings-CufjYTzV.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DK2CKHRD.js → SyncSettings-D_VAYHIg.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-BKiOzeIO.js → Tasks-BE6nYh9k.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CnQpleXj.js → Templates-BfRbttd6.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DwKz0cjm.js → Tenant-YO71CL80.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-A7t_wsR8.js → Terminal-D1mQaE_Z.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BqYY9l44.js → Tokens-CvnNQtX4.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BI4bXFmi.js → Trigger-BnN19FJt.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-yMynKTRG.js → Trust-C6oZLHqp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Br4IScM6.js → UkeySign-BOhFYuWu.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CWcThGsP.js → VideoEditing-NMc6-qc-.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CZcAtjxj.js → Wallet-BALmcKtd.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-BnTZFMA0.js → WebAuthn-6IVe-W6O.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-N7gGz3_n.js → WorkflowEditor-BIv20DXb.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D175ZIO0.js → chat-CiceyA69.js} +1 -1
- package/src/assets/web-panel/assets/{colors-LKhZyttv.js → colors-CxwvpRW0.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CsJSebxT.js → compact-item-N6ASDseQ.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BJ_CPYFC.js → createContext-CQ8PEhyP.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-CzD3IqH8.js → hasIn-DDiIuryC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhiZDGg7.js → index-1jBrqw2R.js} +1 -1
- package/src/assets/web-panel/assets/index-1losWCP0.js +1 -0
- package/src/assets/web-panel/assets/{index-CRGNuUIM.js → index-92sMXPmR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dm-3kvtD.js → index-BCgZTQf8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZluCuTH.js → index-BKLatStI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTIkCKav.js → index-BKmY57ry.js} +1 -1
- package/src/assets/web-panel/assets/{index-E7t1hAnk.js → index-BZ4O1Vp7.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7ZcBI5s.js → index-BdmcwwMp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNVLVzN5.js → index-Bj-mAQFK.js} +1 -1
- package/src/assets/web-panel/assets/{index-R1cFADfk.js → index-BjvirvV4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvF2tC6C.js → index-Bw6UqIkJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-6qPbrYF7.js → index-BzdEj9_B.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMvdoiFr.js → index-C43WIe_p.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjOrt4vw.js → index-CHsNn-Qv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsirlkJ0.js → index-COIxnEwP.js} +1 -1
- package/src/assets/web-panel/assets/{index-D401L3yx.js → index-C_dbCu-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDSZDDb2.js → index-Cb0QVAZL.js} +3 -3
- package/src/assets/web-panel/assets/{index-BV-__mlC.js → index-CcUK2M7W.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXp1jVsK.js → index-ClASVywF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8kltMTW.js → index-CnArbjXg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Kn-Of5ew.js → index-Cur_KKpV.js} +1 -1
- package/src/assets/web-panel/assets/{index-RIO4JKMP.js → index-Cwk0olXV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKjBAdm0.js → index-D0yG93O4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSNibAqz.js → index-D58a5SL3.js} +1 -1
- package/src/assets/web-panel/assets/{index-uTEVWPYA.js → index-DEjAgx2R.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJVkBmSc.js → index-DUKjS4kF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmJdof_c.js → index-DWKigrAM.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2HBKw07.js → index-DcMESTJs.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnPt5OdD.js → index-DgwpVvQq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOO73rHE.js → index-Dj2wgF3A.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5IZCkZG.js → index-DobYLmfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-JTX9A7w0.js → index-Dp3r80qO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyHdYUeZ.js → index-DtpWqJ-2.js} +1 -1
- package/src/assets/web-panel/assets/index-EoP_WtDt.js +1 -0
- package/src/assets/web-panel/assets/{index-BEDFHKO3.js → index-_BHtKVC_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXH9ujMW.js → index-bYorCa3r.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd7dICwB.js → index-hn9LVkIY.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7UYymse.js → index-mmpauJ3E.js} +1 -1
- package/src/assets/web-panel/assets/{index-DM3uBEWD.js → index-xQlQRo2j.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CBW0okek.js → initDefaultProps-1MKZtqXZ.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DGAffQ0Z.js → motion-DFBQpRS8.js} +1 -1
- package/src/assets/web-panel/assets/{move-DFJ0-5IW.js → move-CiI8Ada0.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AvrDghg1.js → omit-CjeFoPcC.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D7csw9i1.js → pickAttrs-Be3kefJq.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-hZ6Lg6kG.js → placementArrow-Be5Ra1_B.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DLLx5VvS.js → responsiveObserve-NCu3YHiX.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BaRIT3ev.js → slide-f23lSB4X.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Cdjyuhrz.js → statusUtils-Dq99US_U.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CbrNybTt.js → styleChecker-B-UUq5Ww.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-B8gAhiRC.js → useFlexGapSupport-D6aUzeVO.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BZPy4ICP.js → useFs-DU-R5a4I.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-6Y0NDMVv.js → vnode-N7r8LSGe.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DTbMGsSH.js → zoom-07xoxB1t.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/__tests__/android.test.js +260 -0
- package/src/commands/__tests__/hub-wechat.test.js +186 -15
- package/src/commands/android.js +284 -0
- package/src/commands/hub.js +263 -19
- package/src/gateways/ws/personal-data-hub-protocol.js +28 -8
- package/src/index.js +2 -0
- package/src/lib/__tests__/cc-android-bridge.test.js +245 -0
- package/src/lib/cc-android-bridge.js +206 -0
- package/src/lib/personal-data-hub-wiring.js +98 -20
- package/src/lib/web-ui-server.js +2 -1
- package/src/assets/web-panel/assets/index-CY1mQA2I.js +0 -1
- 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(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
};
|