chainlesschain 0.162.93 → 0.162.95

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 (168) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BB5mADdi.js → AIOps-DLvvkb9x.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BcIs1DhT.js → ActionButton-BxhQTwqd.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-CNhfaUbF.js → Analytics-C4oUXzhG.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-D1T07CY4.js → AppLayout-CjzKJdv3.js} +5 -5
  7. package/src/assets/web-panel/assets/{Audit-Et29_1F4.js → Audit-B-MrugUs.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-B0ggFwOy.js → Backup-ecIfEpIZ.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-ChTsFhxU.js → BaseInput-BUcSfWnx.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-H5DMwE1E.js → Chat-DIc1n_Lk.js} +6 -6
  11. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CBQYVCUs.js → ChatBubbleRenderer-BGzg7oVA.js} +1 -1
  12. package/src/assets/web-panel/assets/{Checkbox-DBsH3Plc.js → Checkbox-BQ3Ueff-.js} +1 -1
  13. package/src/assets/web-panel/assets/{Codegen-gGtw6sXf.js → Codegen-CvSGLnqE.js} +1 -1
  14. package/src/assets/web-panel/assets/{Col-Df317HUo.js → Col-Br9IXZcE.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-CSERdOno.js → Community-tPuLOf0r.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-D31wUdGx.js → Compact-vMWH73BE.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compliance-92p7-fAe.js → Compliance-Ct9z699x.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cowork-B_HJ_3fc.js → Cowork-Bxs1kL1y.js} +3 -3
  19. package/src/assets/web-panel/assets/{Cron-DIAgns-d.js → Cron-QEpr2k4U.js} +2 -2
  20. package/src/assets/web-panel/assets/{Crosschain--pT2fZrx.js → Crosschain-u4xbUw6O.js} +1 -1
  21. package/src/assets/web-panel/assets/{DID-BR_apx67.js → DID-DXTEvyrW.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dashboard-CIv6YbX3.js → Dashboard-C4pZgzkg.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-BByKNFqx.js → Dropdown-LRuqREGe.js} +1 -1
  24. package/src/assets/web-panel/assets/EmailListRenderer-D7XI1yBM.js +1 -0
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CA9WH0R8.js → FamilyGuardDashboard-3y_SuAMQ.js} +1 -1
  26. package/src/assets/web-panel/assets/{Federation-CnOtOrN0.js → Federation-DbjbJJCW.js} +1 -1
  27. package/src/assets/web-panel/assets/{FormItemContext-CW4kzO3J.js → FormItemContext-WPpMZcUw.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-CUVA-qrZ.js → GenericCardRenderer-nNT9Tn4E.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-Dl9HG9Yo.js → Git-DqDaVzwV.js} +2 -2
  30. package/src/assets/web-panel/assets/{Governance-BgieRKC3.js → Governance-BquBZpcg.js} +1 -1
  31. package/src/assets/web-panel/assets/{Inference-55hVpHm_.js → Inference-RcPPYijs.js} +1 -1
  32. package/src/assets/web-panel/assets/{KnowledgeGraph-BCSm8nT7.js → KnowledgeGraph-ByXfZV13.js} +1 -1
  33. package/src/assets/web-panel/assets/{Logs-DPR2qyoe.js → Logs-CHT3DUcq.js} +2 -2
  34. package/src/assets/web-panel/assets/{Marketplace-DSfYHaLe.js → Marketplace-Bi0HJ9pN.js} +1 -1
  35. package/src/assets/web-panel/assets/{McpTools-DvP_EW02.js → McpTools-QCBxPRRc.js} +4 -4
  36. package/src/assets/web-panel/assets/{Memory-vpNLjkQh.js → Memory-D4RlxGXn.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileBridge-DXT2hsiu.js → MobileBridge-CU_PV7e-.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileProjects-wh6wAgGH.js → MobileProjects-c1CMFD8z.js} +1 -1
  39. package/src/assets/web-panel/assets/{Mtc-hLUm4hEi.js → Mtc-BezDB1-s.js} +5 -5
  40. package/src/assets/web-panel/assets/{MtcAudit-DapBAJjs.js → MtcAudit-CgLpiHpE.js} +6 -6
  41. package/src/assets/web-panel/assets/{Multisig-hQs0FE-E.js → Multisig-vx6bz-bA.js} +3 -3
  42. package/src/assets/web-panel/assets/{NLProgramming-JHOgd9cA.js → NLProgramming-CGOagg3-.js} +1 -1
  43. package/src/assets/web-panel/assets/{Notes-CRF8S0xK.js → Notes-BcuDGYRS.js} +4 -4
  44. package/src/assets/web-panel/assets/{NotificationSettings-VYaLm62h.js → NotificationSettings-ZAGCDUpc.js} +1 -1
  45. package/src/assets/web-panel/assets/{OrderTableRenderer-PNYP-hFh.js → OrderTableRenderer-B5onE0eX.js} +1 -1
  46. package/src/assets/web-panel/assets/{Organization-DnrXiOjr.js → Organization-y_y20-5Y.js} +4 -4
  47. package/src/assets/web-panel/assets/{Overflow-y0VkBEmD.js → Overflow-BEICS31D.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-jK93QK2m.js → P2P-D_WRMzh4.js} +2 -2
  49. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +7 -0
  50. package/src/assets/web-panel/assets/{Permissions-6u2dj6z7.js → Permissions-3aqCc_4f.js} +4 -4
  51. package/src/assets/web-panel/assets/{PersonalDataHub-E0j19ibV.js → PersonalDataHub-DT3R9g9Y.js} +3 -3
  52. package/src/assets/web-panel/assets/{Pipeline-2YSzaqHA.js → Pipeline-MiMWIhw0.js} +1 -1
  53. package/src/assets/web-panel/assets/{Privacy-jhYDq4eK.js → Privacy-C3JsLuDa.js} +1 -1
  54. package/src/assets/web-panel/assets/{ProjectInit-C4UBpmgK.js → ProjectInit-EhHVi1ff.js} +2 -2
  55. package/src/assets/web-panel/assets/{ProjectSettings-B-BYxXeT.js → ProjectSettings-D_dNEA37.js} +2 -2
  56. package/src/assets/web-panel/assets/{Projects-Dn-wcIfk.js → Projects-CXydUXp8.js} +1 -1
  57. package/src/assets/web-panel/assets/{Providers-B9oU5w2Y.js → Providers-CPEhi2iZ.js} +1 -1
  58. package/src/assets/web-panel/assets/{QuickAsk-BSEAjKKg.js → QuickAsk-BFQpMleX.js} +1 -1
  59. package/src/assets/web-panel/assets/{Recommend-B2G2qN0o.js → Recommend-DzbovRC8.js} +1 -1
  60. package/src/assets/web-panel/assets/Reputation-DoBeV3TD.js +1 -0
  61. package/src/assets/web-panel/assets/{Row-CfFRxFir.js → Row-DthhwX_B.js} +1 -1
  62. package/src/assets/web-panel/assets/{RssFeed-Cr7P2tEV.js → RssFeed-Dsf5U0lP.js} +3 -3
  63. package/src/assets/web-panel/assets/{Search-DU23Ncv0.js → Search-CJPB8kKl.js} +1 -1
  64. package/src/assets/web-panel/assets/{Security-XlP_XKyJ.js → Security-DI9MfpdD.js} +4 -4
  65. package/src/assets/web-panel/assets/{Services-CvmT7pHE.js → Services-BdG0TFGf.js} +2 -2
  66. package/src/assets/web-panel/assets/{Skeleton-r38WJidJ.js → Skeleton-DSYazY_B.js} +1 -1
  67. package/src/assets/web-panel/assets/Skills-BD309VTT.js +1 -0
  68. package/src/assets/web-panel/assets/{Sla-D4EOWYSw.js → Sla-CaUlw4_6.js} +1 -1
  69. package/src/assets/web-panel/assets/{SpeechSettings-COVO-nuO.js → SpeechSettings-CZAPkR7H.js} +1 -1
  70. package/src/assets/web-panel/assets/{SyncSettings-CFworQPJ.js → SyncSettings-CFRSowN3.js} +2 -2
  71. package/src/assets/web-panel/assets/Tasks-Di8EOluc.js +1 -0
  72. package/src/assets/web-panel/assets/{Templates-CXcol1Sm.js → Templates-B5FTziwi.js} +1 -1
  73. package/src/assets/web-panel/assets/{Tenant-DthNkjQO.js → Tenant-Dj3XYfBg.js} +1 -1
  74. package/src/assets/web-panel/assets/Terminal-CznH_rJT.js +3 -0
  75. package/src/assets/web-panel/assets/{TimelineRenderer-Dd9CmQ-l.js → TimelineRenderer-DTHzaXPt.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tokens-DoV9Eaxw.js → Tokens-Bo56nuML.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trigger-t0b7L2h7.js → Trigger-pyVUVBwb.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trust-fXDFZevl.js → Trust-Cp7jMkKN.js} +1 -1
  79. package/src/assets/web-panel/assets/{UkeySign-c7zmwDuv.js → UkeySign-aPcZCdpz.js} +1 -1
  80. package/src/assets/web-panel/assets/{VideoEditing-AxUsMrpY.js → VideoEditing-DAv5j--w.js} +1 -1
  81. package/src/assets/web-panel/assets/{Wallet-BB6Hkf9D.js → Wallet-DRhK7IDR.js} +4 -4
  82. package/src/assets/web-panel/assets/{WebAuthn-JBl4_qBv.js → WebAuthn-B2yQJuq8.js} +4 -4
  83. package/src/assets/web-panel/assets/{WorkflowEditor-BMOyglO4.js → WorkflowEditor-D98DhR54.js} +1 -1
  84. package/src/assets/web-panel/assets/{chat-ClEx1qbW.js → chat-BXrRNi8C.js} +1 -1
  85. package/src/assets/web-panel/assets/{colors-DBawfCLx.js → colors-jQ94T_qn.js} +1 -1
  86. package/src/assets/web-panel/assets/{compact-item-BZff7uOC.js → compact-item-KqUJLlF2.js} +1 -1
  87. package/src/assets/web-panel/assets/{createContext-pWYC3gCE.js → createContext-DxM3zuQW.js} +1 -1
  88. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +1 -0
  89. package/src/assets/web-panel/assets/{hasIn-lX6sY5PY.js → hasIn-BGm946KV.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-Dm_BVdZG.js → index-1P0sXNhG.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-DdXLG_Lh.js → index-B-2HpfWo.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-CRhurdyB.js → index-B4b7KjRq.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-B2m7AQfe.js → index-BAPk6ntc.js} +1 -1
  94. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +1 -0
  95. package/src/assets/web-panel/assets/{index-OYqduj3d.js → index-BGjCjsf1.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BdKd25BC.js → index-BMp41Q1Y.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dt3yAuCK.js → index-BXMZLfd8.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-5ER-NP_g.js → index-BaU_fSK7.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BYgqlm3l.js → index-BbS0cBU9.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-oETPgXwU.js → index-BnEZfi5D.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C6QoSIPt.js → index-C5NQai7O.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BkUBHiXd.js → index-C7Z2m28C.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-B0xNvq3Z.js → index-C7sBLech.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DH58L6nn.js → index-CbQvgx2e.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cd3KxxFO.js → index-Ce-YoL4x.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Dj-c7GTQ.js → index-CfE3jxrT.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DE0RpNJf.js → index-CghRL1dh.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Oirvb-h0.js → index-CreXAr5Y.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-B0cwx_ly.js → index-Crw76vIF.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CXBf3t5D.js → index-Cz_SFSix.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-B7xkvbtG.js → index-D1VWL6Lc.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Bxy05j6q.js → index-DOYcemym.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bvcasgi2.js → index-DRzsaWQy.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Bp4RaoTi.js → index-DSzb9VOG.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DQTfwMek.js → index-DTUs9t5F.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-oK90a36q.js → index-DWcd5zDG.js} +3 -3
  117. package/src/assets/web-panel/assets/{index-Cewjdty4.js → index-DZMzj4ay.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-D4tHr9Y9.js → index-DZpVYD4j.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-E7dpMseg.js → index-DboUBf6R.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CEc3MwP8.js → index-Dgzw6eKr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CXBvWyps.js → index-Dib0FsTT.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DPXnMie3.js → index-FSYz5aww.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CdXWNqWZ.js → index-LWTZAud0.js} +1 -1
  124. package/src/assets/web-panel/assets/index-N_oOFphx.js +1 -0
  125. package/src/assets/web-panel/assets/{index-mBBrUDzF.js → index-T4UJiTi3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-F4__gs9X.js → index-aOLXuHpG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CI6hB8lg.js → index-mZNLT2SP.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CLqR3qaE.js → index-yavsyHif.js} +1 -1
  129. package/src/assets/web-panel/assets/{initDefaultProps-BTUpF0QI.js → initDefaultProps-BDoZJI7g.js} +1 -1
  130. package/src/assets/web-panel/assets/{motion-2WeVnnEL.js → motion-sceLjgp-.js} +1 -1
  131. package/src/assets/web-panel/assets/{move-WjKqLduO.js → move-CTWQpFa7.js} +1 -1
  132. package/src/assets/web-panel/assets/{omit-DnFKewM5.js → omit-dr-NCrHZ.js} +1 -1
  133. package/src/assets/web-panel/assets/{pickAttrs-C6Y6tV6C.js → pickAttrs-DK-GX_Z2.js} +1 -1
  134. package/src/assets/web-panel/assets/{placementArrow-5GZUx8Lr.js → placementArrow-BIPf--18.js} +1 -1
  135. package/src/assets/web-panel/assets/{responsiveObserve-DGmwHltc.js → responsiveObserve-BzrWT5tV.js} +1 -1
  136. package/src/assets/web-panel/assets/{slide-RbVe2IlW.js → slide-D5XhxlET.js} +1 -1
  137. package/src/assets/web-panel/assets/{statusUtils-CqyCtZXh.js → statusUtils-6DI-k-9E.js} +1 -1
  138. package/src/assets/web-panel/assets/{styleChecker-D4lnP6aI.js → styleChecker-DMw-eyn9.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFlexGapSupport-DiMKO4Xv.js → useFlexGapSupport-D4oVdliM.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFs--HqUmdpU.js → useFs-D3PPZnvy.js} +1 -1
  141. package/src/assets/web-panel/assets/{usePersonalDataHub-4h3Apiwr.js → usePersonalDataHub-Gh5ba4KP.js} +1 -1
  142. package/src/assets/web-panel/assets/{vnode-BKpoqKhe.js → vnode-CVEM1u4x.js} +1 -1
  143. package/src/assets/web-panel/assets/{zoom-D1RKjeeJ.js → zoom-ClT_-9RZ.js} +1 -1
  144. package/src/assets/web-panel/index.html +1 -1
  145. package/src/commands/pdh.js +162 -0
  146. package/src/harness/mcp-client.js +19 -2
  147. package/src/harness/mock-llm-provider.js +14 -0
  148. package/src/index.js +2 -0
  149. package/src/lib/claude-code-bridge.js +14 -2
  150. package/src/lib/file-checkpoint.js +30 -4
  151. package/src/lib/pdh-bridge.js +266 -0
  152. package/src/lib/repl-completer.js +5 -1
  153. package/src/lib/session-tail.js +6 -1
  154. package/src/lib/skill-mcp.js +10 -4
  155. package/src/repl/agent-repl.js +13 -6
  156. package/src/runtime/agent-core.js +25 -0
  157. package/src/runtime/headless-runner.js +39 -5
  158. package/src/runtime/headless-stream.js +92 -1
  159. package/src/runtime/mcp-config.js +104 -3
  160. package/src/assets/web-panel/assets/EmailListRenderer-DKCmhw1W.js +0 -1
  161. package/src/assets/web-panel/assets/PdhVaultBrowser-BITZbGtU.js +0 -7
  162. package/src/assets/web-panel/assets/Reputation-cdXZrYdH.js +0 -1
  163. package/src/assets/web-panel/assets/Skills-Bd49FHct.js +0 -1
  164. package/src/assets/web-panel/assets/Tasks-mWQgh-Xq.js +0 -1
  165. package/src/assets/web-panel/assets/Terminal-BuX3TG_s.js +0 -3
  166. package/src/assets/web-panel/assets/devWarning-B1b-Rehg.js +0 -1
  167. package/src/assets/web-panel/assets/index-Bv1uJXTd.js +0 -1
  168. package/src/assets/web-panel/assets/index-DJYUttUZ.js +0 -1
@@ -43,6 +43,33 @@ function ensureDir(dir) {
43
43
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
44
44
  }
45
45
 
46
+ /**
47
+ * Write a file atomically: write to a unique temp sibling, then rename over the
48
+ * target. rename() is atomic within a filesystem, so a crash mid-write leaves
49
+ * the previous file (or nothing) intact — never a half-written manifest/blob.
50
+ * Without this, a checkpoint manifest truncated by a crash makes the whole
51
+ * checkpoint unrecoverable (getCheckpoint's JSON.parse fails → null) while its
52
+ * blobs are orphaned on disk. The temp lives in the SAME dir as the target so
53
+ * the rename stays on one filesystem; on failure the temp is best-effort
54
+ * removed. Temp names end in `.tmp` (never `.json`), so a leftover from a hard
55
+ * crash is ignored by listCheckpoints.
56
+ */
57
+ function atomicWriteFileSync(filePath, data) {
58
+ const rand = Math.random().toString(36).slice(2, 8);
59
+ const tmp = `${filePath}.${process.pid}.${rand}.tmp`;
60
+ try {
61
+ fs.writeFileSync(tmp, data);
62
+ fs.renameSync(tmp, filePath);
63
+ } catch (err) {
64
+ try {
65
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
66
+ } catch {
67
+ /* best-effort temp cleanup */
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+
46
73
  function sha256(buf) {
47
74
  return createHash("sha256").update(buf).digest("hex");
48
75
  }
@@ -127,7 +154,7 @@ export function createCheckpoint(paths, opts = {}) {
127
154
  const buf = fs.readFileSync(abs);
128
155
  const hash = sha256(buf);
129
156
  const blobPath = path.join(blobDir, hash);
130
- if (!fs.existsSync(blobPath)) fs.writeFileSync(blobPath, buf);
157
+ if (!fs.existsSync(blobPath)) atomicWriteFileSync(blobPath, buf);
131
158
  files.push({
132
159
  rel: path.relative(cwd, abs) || path.basename(abs),
133
160
  abs,
@@ -145,10 +172,9 @@ export function createCheckpoint(paths, opts = {}) {
145
172
  files,
146
173
  };
147
174
  ensureDir(root);
148
- fs.writeFileSync(
175
+ atomicWriteFileSync(
149
176
  path.join(root, `${id}.json`),
150
177
  JSON.stringify(manifest, null, 2),
151
- "utf-8",
152
178
  );
153
179
  return manifest;
154
180
  }
@@ -275,7 +301,7 @@ export function restoreCheckpoint(id, opts = {}) {
275
301
 
276
302
  for (const w of toWrite) {
277
303
  ensureDir(path.dirname(w.abs));
278
- fs.writeFileSync(w.abs, w.blob);
304
+ atomicWriteFileSync(w.abs, w.blob);
279
305
  restored.push(w.rel);
280
306
  }
281
307
 
@@ -0,0 +1,266 @@
1
+ /**
2
+ * PDH bridge discovery (Phase 0) — find a first-party "device capability" MCP
3
+ * server that the Android app advertises via a lockfile, and turn it into an
4
+ * MCP server config the agent loop can connect to.
5
+ *
6
+ * This is the cc-side (CLI) half of the Personal-Data IDE / PDH Bridge
7
+ * (design: docs/design/modules/101_个人数据IDE桥接方案.md). It mirrors the IDE
8
+ * bridge (ide-bridge.js, design module 98): the app runs a localhost MCP server
9
+ * exposing collect / query / file / task tools, and writes
10
+ * <appFiles>/.chainlesschain/pdh-bridge/<port>.json (file 0600, dir 0700)
11
+ * The app ALSO injects CHAINLESSCHAIN_PDH_PORT (+ optional _TOKEN) into the env
12
+ * of the in-APK cc it spawns, so cc locks onto the exact instance without
13
+ * scanning ("path A"). External agents (e.g. desktop over a forwarded port)
14
+ * fall back to a lockfile scan ("path B").
15
+ *
16
+ * Unlike the IDE bridge there is no multi-root workspace concept: a device has
17
+ * a single PDH server, so discovery is env-port → else the newest live lock.
18
+ *
19
+ * Pure CLI, no Android dependency, fully unit-testable through `_deps`. The
20
+ * connect path reuses the existing MCP client (HTTP) via mcp-config.js
21
+ * `loadPdhMcp`. Reserved server name `pdh` → tools `mcp__pdh__*`.
22
+ *
23
+ * NOTE: the discovery mechanics (localhost / transport / stale filtering,
24
+ * env fast-path, bearer headers) duplicate ide-bridge.js by design — keeping
25
+ * the two bridges decoupled for Phase 0. A later refactor may extract a shared
26
+ * device-bridge core; until then this file is self-contained on purpose.
27
+ */
28
+ import fs from "fs";
29
+ import path from "path";
30
+ import os from "os";
31
+
32
+ /** A lock whose pid is dead and whose file is older than this = stale. */
33
+ const STALE_TTL_MS = 30_000;
34
+
35
+ /** Transports the CLI's MCP client can actually talk (see mcp-client.js). */
36
+ const SUPPORTED_TRANSPORTS = new Set(["http", "https", "sse"]);
37
+
38
+ /** Lock `kind` we accept (absent is tolerated; the dir already scopes us). */
39
+ const EXPECTED_KIND = "pdh-bridge";
40
+
41
+ /** Liveness probe via signal 0 — cross-platform (works on Windows too). */
42
+ function defaultProcessAlive(pid) {
43
+ if (!pid || typeof pid !== "number") return false;
44
+ try {
45
+ process.kill(pid, 0);
46
+ return true;
47
+ } catch (err) {
48
+ // EPERM: the process exists but we may not signal it → still alive.
49
+ return !!(err && err.code === "EPERM");
50
+ }
51
+ }
52
+
53
+ export const _deps = {
54
+ readDir: (d) => fs.readdirSync(d),
55
+ readFile: (p) => fs.readFileSync(p, "utf-8"),
56
+ statMtimeMs: (p) => fs.statSync(p).mtimeMs,
57
+ homedir: () => os.homedir(),
58
+ now: () => Date.now(),
59
+ processAlive: defaultProcessAlive,
60
+ };
61
+
62
+ /** `~/.chainlesschain/pdh-bridge/`. */
63
+ export function pdhLockDir(deps = _deps) {
64
+ return path.join(deps.homedir(), ".chainlesschain", "pdh-bridge");
65
+ }
66
+
67
+ function isLocalhostUrl(url) {
68
+ try {
69
+ const u = new URL(url);
70
+ return u.hostname === "127.0.0.1" || u.hostname === "::1";
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function inferTransportFromUrl(url) {
77
+ if (/\/sse(\b|\/|$)/i.test(url)) return "sse";
78
+ if (/^https:/i.test(url)) return "https";
79
+ if (/^http:/i.test(url)) return "http";
80
+ return null;
81
+ }
82
+
83
+ /** Parse + validate one lock JSON. Returns a normalized lock or null. */
84
+ function parseLock(raw, filePath) {
85
+ let lock;
86
+ try {
87
+ lock = JSON.parse(raw);
88
+ } catch {
89
+ return null;
90
+ }
91
+ if (!lock || typeof lock !== "object") return null;
92
+ // Reject a foreign lock kind if present; absent kind is tolerated (the
93
+ // pdh-bridge/ dir already scopes us, but a stray file gets rejected here).
94
+ if (lock.kind != null && String(lock.kind) !== EXPECTED_KIND) return null;
95
+ if (!lock.url || !isLocalhostUrl(lock.url)) return null;
96
+
97
+ const transport = String(
98
+ lock.transport || inferTransportFromUrl(lock.url) || "",
99
+ ).toLowerCase();
100
+ if (!SUPPORTED_TRANSPORTS.has(transport)) return null; // e.g. ws — not yet
101
+
102
+ return {
103
+ kind: EXPECTED_KIND,
104
+ device: typeof lock.device === "string" ? lock.device : "unknown",
105
+ appUid: typeof lock.appUid === "number" ? lock.appUid : null,
106
+ transport,
107
+ url: lock.url,
108
+ port: lock.port,
109
+ token: typeof lock.token === "string" ? lock.token : null,
110
+ pid: typeof lock.pid === "number" ? lock.pid : null,
111
+ startedAt: typeof lock.started_at === "number" ? lock.started_at : 0,
112
+ _file: filePath,
113
+ };
114
+ }
115
+
116
+ /** True when a lock is dead: pid gone AND file older than the TTL. */
117
+ function isStale(lock, deps) {
118
+ if (lock.pid && deps.processAlive(lock.pid)) return false;
119
+ let mtime;
120
+ try {
121
+ mtime = deps.statMtimeMs(lock._file);
122
+ } catch {
123
+ return true;
124
+ }
125
+ return deps.now() - mtime > STALE_TTL_MS;
126
+ }
127
+
128
+ /**
129
+ * Scan the lock dir → normalized, live, localhost, supported-transport locks.
130
+ * Missing dir / unreadable files are silently skipped (best-effort).
131
+ */
132
+ export function readPdhLocks(deps = _deps) {
133
+ const dir = pdhLockDir(deps);
134
+ let names;
135
+ try {
136
+ names = deps.readDir(dir);
137
+ } catch {
138
+ return [];
139
+ }
140
+ const out = [];
141
+ for (const name of names || []) {
142
+ if (!String(name).endsWith(".json")) continue;
143
+ const fp = path.join(dir, name);
144
+ let raw;
145
+ try {
146
+ raw = deps.readFile(fp);
147
+ } catch {
148
+ continue;
149
+ }
150
+ const lock = parseLock(raw, fp);
151
+ if (!lock) continue;
152
+ if (isStale(lock, deps)) continue;
153
+ out.push(lock);
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /** Is the env wired to a PDH bridge (app-spawned cc, or explicit opt-in)? */
159
+ export function isInPdhTerminal(env = process.env) {
160
+ if (!env) return false;
161
+ if (env.CHAINLESSCHAIN_PDH_PORT) return true;
162
+ if (env.CHAINLESSCHAIN_PDH) return true;
163
+ return false;
164
+ }
165
+
166
+ /**
167
+ * Pick the PDH server to connect to, or null.
168
+ *
169
+ * Path A (deterministic): if `CHAINLESSCHAIN_PDH_PORT` is set and a live lock
170
+ * has that port, use it (token may come from the lock or the env).
171
+ * Path B (scan): otherwise the newest live lock (a device has one PDH server;
172
+ * no workspace concept to match on, unlike the IDE bridge).
173
+ *
174
+ * @param {object} opts { env?, force? } (force kept for API symmetry)
175
+ * @param {object} [deps]
176
+ */
177
+ export function discoverPdhServer(
178
+ { env = process.env, force = false } = {},
179
+ deps = _deps,
180
+ ) {
181
+ void force; // no workspace gating for PDH; param kept for call-site symmetry
182
+ const locks = readPdhLocks(deps);
183
+ if (!locks.length) return null;
184
+
185
+ // Path A — env fast-path.
186
+ const envPort = env && env.CHAINLESSCHAIN_PDH_PORT;
187
+ if (envPort) {
188
+ const hit = locks.find((l) => String(l.port) === String(envPort));
189
+ if (hit) {
190
+ if (!hit.token && env.CHAINLESSCHAIN_PDH_TOKEN) {
191
+ hit.token = env.CHAINLESSCHAIN_PDH_TOKEN;
192
+ }
193
+ return hit;
194
+ }
195
+ // env named a port with no live lock — fall through to newest.
196
+ }
197
+
198
+ // Path B — newest live lock.
199
+ return locks.slice().sort((a, b) => b.startedAt - a.startedAt)[0];
200
+ }
201
+
202
+ /**
203
+ * A discovered lock → an MCP server config row for `setupMcpFromConfig`.
204
+ * `longRunning` flags the server as exempt from the agent loop's per-call
205
+ * timeout: PDH collect tools can block on `assist_required` (the user does a
206
+ * step in the target app, then resumes) — the same blocking-tool contract as
207
+ * the IDE bridge's `openDiff`.
208
+ */
209
+ export function pdhServerToMcpConfig(lock) {
210
+ if (!lock || !lock.url) return null;
211
+ const headers = {};
212
+ if (lock.token) headers.Authorization = `Bearer ${lock.token}`;
213
+ return {
214
+ url: lock.url,
215
+ transport: lock.transport,
216
+ headers,
217
+ longRunning: true,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Human-readable explanation of why discovery did/didn't find a server — backs
223
+ * `cc pdh doctor`. Returns { inPdhTerminal, lockDir, locks:[...], chosen, reason }.
224
+ */
225
+ export function diagnosePdh(
226
+ { env = process.env, force = false } = {},
227
+ deps = _deps,
228
+ ) {
229
+ const inPdhTerminal = isInPdhTerminal(env);
230
+ const locks = readPdhLocks(deps);
231
+ const chosen = discoverPdhServer({ env, force }, deps);
232
+
233
+ let reason;
234
+ if (chosen) {
235
+ reason =
236
+ env &&
237
+ env.CHAINLESSCHAIN_PDH_PORT &&
238
+ String(chosen.port) === String(env.CHAINLESSCHAIN_PDH_PORT)
239
+ ? "matched CHAINLESSCHAIN_PDH_PORT (env fast-path)"
240
+ : "newest live lock";
241
+ } else if (!locks.length) {
242
+ reason =
243
+ "no live PDH lockfiles in " +
244
+ pdhLockDir(deps) +
245
+ " (is the app's PDH bridge server running? are locks stale?)";
246
+ } else {
247
+ reason = "lockfiles present but none usable";
248
+ }
249
+ return {
250
+ inPdhTerminal,
251
+ lockDir: pdhLockDir(deps),
252
+ locks: locks.map((l) => ({
253
+ device: l.device,
254
+ appUid: l.appUid,
255
+ port: l.port,
256
+ transport: l.transport,
257
+ url: l.url,
258
+ hasToken: !!l.token,
259
+ pid: l.pid,
260
+ })),
261
+ chosen: chosen
262
+ ? { device: chosen.device, port: chosen.port, url: chosen.url }
263
+ : null,
264
+ reason,
265
+ };
266
+ }
@@ -128,8 +128,12 @@ export function makeAtCompleter(opts = {}) {
128
128
  });
129
129
  };
130
130
 
131
+ // De-duplicate before sorting so a drifted/aliased entry in the hand-
132
+ // maintained command list never shows twice in the autocomplete (mirrors the
133
+ // @-path's `new Set` merge below; Claude-Code 2.1.183 fixed the same class of
134
+ // duplicate-in-slash-autocomplete bug).
131
135
  const slashCommands = Array.isArray(opts.slashCommands)
132
- ? [...opts.slashCommands].sort()
136
+ ? [...new Set(opts.slashCommands)].sort()
133
137
  : [];
134
138
 
135
139
  const completer = (line) => {
@@ -89,6 +89,10 @@ export async function* followSession(sessionId, options = {}) {
89
89
  ? fromOffset
90
90
  : initialOffset(sessionId, { fromStart });
91
91
  let buffer = "";
92
+ // Streaming decoder persisted across polls: when the file grows by a partial
93
+ // multi-byte UTF-8 character between reads (the rest arrives next poll), the
94
+ // held bytes are reassembled instead of corrupted into U+FFFD.
95
+ let decoder = new TextDecoder("utf-8");
92
96
 
93
97
  // eslint-disable-next-line no-constant-condition
94
98
  while (true) {
@@ -100,6 +104,7 @@ export async function* followSession(sessionId, options = {}) {
100
104
  // File was truncated / rotated — restart from beginning
101
105
  offset = 0;
102
106
  buffer = "";
107
+ decoder = new TextDecoder("utf-8"); // drop any held partial-char bytes
103
108
  }
104
109
  if (stat.size > offset) {
105
110
  const fd = await fsp.open(filePath, "r");
@@ -108,7 +113,7 @@ export async function* followSession(sessionId, options = {}) {
108
113
  const buf = Buffer.alloc(length);
109
114
  await fd.read(buf, 0, length, offset);
110
115
  offset = stat.size;
111
- buffer += buf.toString("utf-8");
116
+ buffer += decoder.decode(buf, { stream: true });
112
117
  } finally {
113
118
  await fd.close();
114
119
  }
@@ -118,10 +118,16 @@ export async function mountSkillMcpServers(mcpClient, skill, opts = {}) {
118
118
  for (const server of declared) {
119
119
  const normalized = validateMcpServerConfig(server);
120
120
  if (!normalized) {
121
- skipped.push({
122
- name: server?.name || "(invalid)",
123
- error: "invalid config",
124
- });
121
+ const name = server?.name || "(invalid)";
122
+ skipped.push({ name, error: "invalid config" });
123
+ // Surface invalid configs too — previously only connect FAILURES warned,
124
+ // so a malformed server config was dropped silently and the skill author
125
+ // never learned why their MCP server didn't mount.
126
+ if (typeof opts.onWarn === "function") {
127
+ opts.onWarn(
128
+ `[skill-mcp] Skipped "${name}" for skill "${skill?.id || skill?.name}": invalid config`,
129
+ );
130
+ }
125
131
  continue;
126
132
  }
127
133
  try {
@@ -1946,6 +1946,11 @@ export async function startAgentRepl(options = {}) {
1946
1946
  messages.length = 1; // keep system prompt
1947
1947
  messages.push(...rebuilt.filter((m) => m.role !== "system"));
1948
1948
  sessionId = resumeId;
1949
+ // The prior session's checkpoint marks (turn-index → checkpoint id)
1950
+ // no longer map to this swapped-in history; keeping them would make
1951
+ // a later /rewind restore files from the WRONG session's snapshot.
1952
+ // Same reset /clear does on a context swap.
1953
+ _checkpointMarks.length = 0;
1949
1954
  logger.info(
1950
1955
  `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
1951
1956
  );
@@ -1959,6 +1964,8 @@ export async function startAgentRepl(options = {}) {
1959
1964
  messages.length = 1; // keep system prompt
1960
1965
  messages.push(...parsed.filter((m) => m.role !== "system"));
1961
1966
  sessionId = existing.id;
1967
+ // Drop the prior session's checkpoint marks (see JSONL branch).
1968
+ _checkpointMarks.length = 0;
1962
1969
  logger.info(
1963
1970
  `Resumed session ${sessionId} (${parsed.length} messages)`,
1964
1971
  );
@@ -2660,7 +2667,7 @@ export async function startAgentRepl(options = {}) {
2660
2667
 
2661
2668
  // `/sessions` — list recent RESUMABLE conversations (read-only; the ids
2662
2669
  // work with `cc agent --resume <id>`). `/session` shows the current one.
2663
- if (trimmed === "/sessions" || trimmed === "/sessions ") {
2670
+ if (trimmed === "/sessions" || trimmed.startsWith("/sessions ")) {
2664
2671
  try {
2665
2672
  const { listRecentSessions } = await import("../lib/recent-session.js");
2666
2673
  const { renderRecentSessions } = await import("./recent-sessions.js");
@@ -2676,7 +2683,7 @@ export async function startAgentRepl(options = {}) {
2676
2683
  // `/memory` — project-memory files auto-loaded into the system prompt
2677
2684
  // (cc.md hierarchy + imports + path-scoped rules). Distinct from `#` (add
2678
2685
  // a note) and `cc memory recall` (scoped store).
2679
- if (trimmed === "/memory" || trimmed === "/memory ") {
2686
+ if (trimmed === "/memory" || trimmed.startsWith("/memory ")) {
2680
2687
  try {
2681
2688
  const { loadProjectInstructions } =
2682
2689
  await import("../lib/project-instructions.js");
@@ -2695,7 +2702,7 @@ export async function startAgentRepl(options = {}) {
2695
2702
  }
2696
2703
 
2697
2704
  // `/mcp` — overview of connected MCP servers' resources + prompts.
2698
- if (trimmed === "/mcp" || trimmed === "/mcp ") {
2705
+ if (trimmed === "/mcp" || trimmed.startsWith("/mcp ")) {
2699
2706
  const mcpClient = _adhocMcp?.mcpClient || _bundleMcpClient;
2700
2707
  logger.log(renderMcpSurface(mcpClient));
2701
2708
  prompt();
@@ -2753,7 +2760,7 @@ export async function startAgentRepl(options = {}) {
2753
2760
 
2754
2761
  // `/doctor` — consolidated session-health readout (Claude-Code parity):
2755
2762
  // provider/key/IDE/MCP/permissions/hooks in one pass-or-warn view.
2756
- if (trimmed === "/doctor" || trimmed === "/doctor ") {
2763
+ if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
2757
2764
  let config = {};
2758
2765
  try {
2759
2766
  config = (await import("../lib/config-manager.js")).loadConfig() || {};
@@ -2860,7 +2867,7 @@ export async function startAgentRepl(options = {}) {
2860
2867
 
2861
2868
  // `/cost` — running token spend + estimated $ for this session (Claude-Code
2862
2869
  // parity). In-memory accumulation, so it works without session persistence.
2863
- if (trimmed === "/cost" || trimmed === "/cost ") {
2870
+ if (trimmed === "/cost" || trimmed.startsWith("/cost ")) {
2864
2871
  let overrides;
2865
2872
  let visionModel;
2866
2873
  try {
@@ -2937,7 +2944,7 @@ export async function startAgentRepl(options = {}) {
2937
2944
 
2938
2945
  // `/ide` — IDE bridge connection status (Claude-Code parity): which editor
2939
2946
  // is connected, its tools, or why discovery came up empty.
2940
- if (trimmed === "/ide" || trimmed === "/ide ") {
2947
+ if (trimmed === "/ide" || trimmed.startsWith("/ide ")) {
2941
2948
  let diag = null;
2942
2949
  try {
2943
2950
  const { diagnoseIde } = await import("../lib/ide-bridge.js");
@@ -3838,6 +3838,11 @@ export async function* agentLoop(messages, options) {
3838
3838
  // True once a Stop hook has forced a continuation — passed to the next Stop
3839
3839
  // hook as `stop_hook_active` so a well-behaved hook won't block forever.
3840
3840
  let stopHookActive = false;
3841
+ // True once we have already re-prompted a thinking-only turn (a turn that
3842
+ // produced extended-thinking but no visible text and no tool calls). The
3843
+ // one-shot guard means a model that keeps returning empty turns completes
3844
+ // rather than looping forever (the iteration budget is the hard backstop).
3845
+ let emptyThinkingReprompted = false;
3841
3846
 
3842
3847
  while (budget.hasRemaining()) {
3843
3848
  budget.consume();
@@ -4036,6 +4041,26 @@ export async function* agentLoop(messages, options) {
4036
4041
  .join("")
4037
4042
  .trim()
4038
4043
  : "";
4044
+ // Claude Code 2.1.183 parity: a turn that produced ONLY extended-thinking
4045
+ // (no visible text, no tool calls) would otherwise complete silently with
4046
+ // an empty answer — the user sees nothing. Re-prompt the model ONCE to
4047
+ // surface its actual response. Mirrors the Stop-hook continuation path
4048
+ // below (push assistant turn + a user nudge, then continue). Scoped to the
4049
+ // thinking-only case so a genuinely empty completion (no thinking) still
4050
+ // ends instead of looping.
4051
+ const _contentEmpty = !String(msg.content || "").trim();
4052
+ if (_contentEmpty && _thinking && !emptyThinkingReprompted) {
4053
+ emptyThinkingReprompted = true;
4054
+ messages.push({ role: "assistant", content: "" });
4055
+ messages.push({
4056
+ role: "user",
4057
+ content:
4058
+ "You ended your turn with only internal reasoning and no visible " +
4059
+ "response. Please provide your actual answer now.",
4060
+ });
4061
+ yield { type: "empty-thinking-reprompt", runId };
4062
+ continue;
4063
+ }
4039
4064
  yield {
4040
4065
  type: "response-complete",
4041
4066
  content: msg.content || "",
@@ -414,6 +414,40 @@ export async function runAgentHeadless(options = {}, deps = {}) {
414
414
  writeErr("No previous session to continue; starting a new one.\n");
415
415
  }
416
416
 
417
+ // Machine-readable modes (`--output-format json` / `stream-json`) must ALWAYS
418
+ // end with a terminal result envelope — a consumer parsing stdout otherwise
419
+ // gets nothing and can't tell success from failure. Several setup-phase error
420
+ // paths below early-return BEFORE the main loop's try/catch envelope (and
421
+ // before `system/init`), so route them through this so stdout is never empty.
422
+ // No-op in text mode (those paths already writeErr a human message).
423
+ const emitHeadlessError = (resultMsg) => {
424
+ if (isStream) {
425
+ writeOut(
426
+ JSON.stringify({
427
+ type: "result",
428
+ subtype: "error",
429
+ is_error: true,
430
+ error: resultMsg,
431
+ }) + "\n",
432
+ );
433
+ } else if (isJson) {
434
+ writeOut(
435
+ JSON.stringify(
436
+ buildResultEnvelope({
437
+ subtype: "error",
438
+ isError: true,
439
+ result: resultMsg,
440
+ sessionId,
441
+ toolCalls: [],
442
+ usage: {},
443
+ numTurns: 0,
444
+ durationMs: 0,
445
+ }),
446
+ ) + "\n",
447
+ );
448
+ }
449
+ };
450
+
417
451
  // Load prior conversation when resuming an existing session. The fresh
418
452
  // system prompt always leads; we drop any persisted system turns so it is
419
453
  // never duplicated.
@@ -489,11 +523,9 @@ export async function runAgentHeadless(options = {}, deps = {}) {
489
523
  writeErr(
490
524
  `[hook] prompt blocked${ups.reason ? ": " + ups.reason : ""}\n`,
491
525
  );
492
- return {
493
- exitCode: 2,
494
- result: ups.reason || "blocked by UserPromptSubmit hook",
495
- isError: true,
496
- };
526
+ const reason = ups.reason || "blocked by UserPromptSubmit hook";
527
+ emitHeadlessError(reason);
528
+ return { exitCode: 2, result: reason, isError: true };
497
529
  }
498
530
  if (ups.additionalContext) {
499
531
  userContent += `\n\n[hook context]\n${ups.additionalContext}`;
@@ -593,6 +625,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
593
625
  }
594
626
  } catch (err) {
595
627
  writeErr(`Error: ${err.message}\n`);
628
+ emitHeadlessError(err.message);
596
629
  return { exitCode: 1, result: err.message, isError: true };
597
630
  }
598
631
  }
@@ -635,6 +668,7 @@ export async function runAgentHeadless(options = {}, deps = {}) {
635
668
  } catch (err) {
636
669
  writeErr(`Error: ${err.message}\n`);
637
670
  if (mcp?.mcpClient) await mcp.mcpClient.disconnectAll().catch(() => {});
671
+ emitHeadlessError(err.message);
638
672
  return { exitCode: 1, result: err.message, isError: true };
639
673
  }
640
674
  if (approvalGate && typeof approvalGate.setConfirmer === "function") {