chainlesschain 0.162.34 → 0.162.36
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-BYfi9NYS.js → AIOps-vAVAFNJ4.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BiS_tAN7.js → ActionButton-BnRHFCKM.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-jiWl_p-B.js → Analytics-BOjwqWqG.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-m4sIzDot.js → AppLayout-Dc0D1Txn.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-CPla3Erm.js → Audit-dd_2efaZ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BGeQzTaB.js → Backup-HF1jgm8G.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DTf7Z1iU.js → BaseInput-CCtzmoKe.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DPTlQlD-.js → Chat-BNfH1c3p.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BgRXce4e.js → ChatBubbleRenderer-DCWFqmI4.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DY-XuQMu.js → Checkbox-BOr-NscK.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B6oxPiZI.js → Codegen-DE058N7-.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dqxb4wSE.js → Col-SOREo1XE.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DCIX514p.js → Community-sOvNZo9f.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGtCzDoJ.js → Compact-DnBe558D.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-zcOYd55o.js → Compliance-o-r6CUbg.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DVTtdIdM.js → Cowork-D6_k9mHP.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-CPUaR69k.js → Cron-CEV3Xkrm.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DnjUS6QH.js → Crosschain-eJ1lQWKU.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Dnz8VDmx.js → DID-B-WqM9Hp.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CtWf27j7.js → Dashboard-ZnKPcsHN.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B4GC1ZV4.js → Dropdown-B8uLWDIP.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-wjij3kzr.js → EmailListRenderer-Jmj2Y7aH.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-rS-2W4u5.js → FamilyGuardDashboard-Cb2xetG-.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-90p5Tnoz.js → Federation-C_07GXoq.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cnrw7gzq.js → FormItemContext-D3kbYrMU.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C85NsWa3.js → GenericCardRenderer-9xgqvGPg.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BFAVM9F8.js → Git-BlwWlMMB.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DBoRonpq.js → Governance-DxN3wQZ_.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DHRyD66j.js → Inference-ls7pSw_D.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CTvUKecD.js → KnowledgeGraph-_n9hYuPI.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CB0dv_Ts.js → Logs-CvEVY5TK.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CN7Hm5Uw.js → Marketplace-C3qvQJT7.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-q5H25_8L.js → McpTools-DiwKpnKx.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BCV3pZ1d.js → Memory-CIBPi_da.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C04Mngt4.js → MobileBridge-D-v0Se8y.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-cP1apTQD.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-ByAMz2DN.js → Mtc-BMFWrI65.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-B7V7byJq.js → MtcAudit-2s8LaHtR.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DtKmcVQV.js → Multisig-dL_nvj7d.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CaMbT5SC.js → NLProgramming-BbrJp06R.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DRjbSTCU.js → Notes-jR9irwy3.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-B9YbJID5.js → NotificationSettings-Dk-STCIX.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BcI_-vGS.js → OrderTableRenderer-CqqfY6zq.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-oTask4BE.js → Organization-BCK5jylo.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bab06ey7.js → Overflow-BRAY7Smt.js} +1 -1
- package/src/assets/web-panel/assets/{P2P--wlBeU0N.js → P2P-BltVRGjb.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-D4t77Pwc.js → PdhVaultBrowser-CV8UbXHe.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-B3sf6CJ3.js → Permissions-_tNl47Qh.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BXOojk63.js → PersonalDataHub-Cgc4HjpX.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DReqtBFN.js → Pipeline-Bn_QU4mu.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-cT1GwKLx.js → Privacy-jzJowp5P.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BhTAzVhH.js → ProjectInit-B_1pJ8qd.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CK-D8Fyj.js → ProjectSettings-CPVZpXzs.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CbHiwen6.js → Projects-CQsHOWnT.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B-ftiXa8.js → Providers-CzzMiLC0.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CT5XPwTF.js → QuickAsk-MxBKIn9o.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CohhlBZ_.js → Recommend-D8lN6Lis.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CrgbixFz.js → Reputation-CfYK-IrV.js} +1 -1
- package/src/assets/web-panel/assets/{Row-ClExmBn3.js → Row-Bg7NZDP9.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-VV0qizCJ.js → RssFeed-BOVNJhj0.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CqJapSiL.js → Search-B38qzmhY.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DY66Zie6.js → Security-CjqleZpe.js} +4 -4
- package/src/assets/web-panel/assets/{Services-RQwxat7-.js → Services-Bu9JSJap.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-0v37UTU_.js → Skeleton-B2RvRkaX.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-B4Vm4DxN.js → Skills-_h42mxMN.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CggphTlo.js → Sla-BssLs56D.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BAOU08C7.js → SpeechSettings-DCxFYHsd.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DmtC4J1w.js → SyncSettings-D2xQuNLE.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DhpOGOlo.js +1 -0
- package/src/assets/web-panel/assets/{Templates-C1QK0YoU.js → Templates-CYG-R-aS.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CieOfmqp.js → Tenant-BQRYLsvP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DWdhrxRq.js → Terminal-imKU7N5j.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CjFVUUDU.js → TimelineRenderer-BIZzBftk.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Bwbk3id9.js → Tokens-uMLH5p_a.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-uJle_yj4.js → Trigger-BzS6XPqx.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BcOuxAA5.js → Trust-R4zhHufZ.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DUu7Ufg6.js → UkeySign-DATQCoGe.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Ck8JtQ2n.js → VideoEditing-ClUmKOtS.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B3jw43on.js → Wallet-DzJTbQzD.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Baf9K0y7.js → WebAuthn-CrXrLmzQ.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CTEDl_83.js → WorkflowEditor-CpvZ0Tma.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CKV51quV.js → chat-a6wpYmVL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BO_RP_yz.js → colors-CXJADb1t.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BZsxw_ZG.js → compact-item-CL2pohS_.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CAbvtzVL.js → createContext-xFi_1G5_.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-QmHT8zDz.js → hasIn-Bchh1rAi.js} +1 -1
- package/src/assets/web-panel/assets/{index-fnDgExTu.js → index-B3Tpv7-d.js} +1 -1
- package/src/assets/web-panel/assets/index-B4l4vLTB.js +1 -0
- package/src/assets/web-panel/assets/{index-BEJa1FiF.js → index-B4zNisy9.js} +1 -1
- package/src/assets/web-panel/assets/{index-jd2r-T4p.js → index-B6NehWty.js} +1 -1
- package/src/assets/web-panel/assets/index-B7Ek5iiY.js +1 -0
- package/src/assets/web-panel/assets/{index-BPZHeug4.js → index-B7knYOpm.js} +1 -1
- package/src/assets/web-panel/assets/{index-GPY0LjCu.js → index-B7wT5VRi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKnngF_f.js → index-BF4xx1_b.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRNYA0BV.js → index-BH9t10pe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKquNxL2.js → index-BPH5ESqs.js} +3 -3
- package/src/assets/web-panel/assets/{index-CEh2Ry_A.js → index-BmsIKzyu.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dob6B6qS.js → index-BoaRB-4a.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ha2_56mf.js → index-BrbJBnT-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dln_vjSY.js → index-C2eMYASq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqiKnXtL.js → index-C4yBRKT4.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3fwyCjJ.js → index-CGq4HQno.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2aiE8jk.js → index-CMybtJY6.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5zhcul9.js → index-CR3kFPuC.js} +1 -1
- package/src/assets/web-panel/assets/{index-8BMLlHCv.js → index-CTRd7vkq.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6i3reUS.js → index-CdU8BwRW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNvTNZ1V.js → index-Cua_P8St.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjrDGJP2.js → index-CuehgDOp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCsZiq4i.js → index-D-TT9Swq.js} +1 -1
- package/src/assets/web-panel/assets/{index-qPafbZmr.js → index-DEYcLAl7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeC7lehI.js → index-DQ_hw_5P.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnPBG3Tr.js → index-DTEu7TSF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8CHQnPl.js → index-DVo1GJoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-9IqJODII.js → index-DjdOL159.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBCYOypV.js → index-DsbMVBj1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BL7gQAuB.js → index-DxahxRP7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC1CFfQU.js → index-EPERz4Pu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVoYeZ5Q.js → index-IkvkNxbc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsBx0u5G.js → index-KCib1PTw.js} +1 -1
- package/src/assets/web-panel/assets/{index-5hlO2-JQ.js → index-M8SZI11a.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSaI8R_7.js → index-TxbHusq2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6AA-xB2.js → index-dsLc7t6W.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRK0oAV5.js → index-jMcv1u5o.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9Z83FTS.js → index-majCS3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3K1eHDd.js → index-u8K1y_lh.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Bc2GWeWe.js → initDefaultProps-DYn3Gc09.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BI-Rxw6o.js → motion-ZS3eolb9.js} +1 -1
- package/src/assets/web-panel/assets/{move-DRPdwDQB.js → move-CEw4uqr3.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B4XTl3jW.js → omit-DlHFZnPp.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Do5d86Wr.js → pickAttrs-eZQvV5fA.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B8VGZ0ZF.js → placementArrow-B31jQwa-.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cf0kI_vN.js → responsiveObserve-DAsNmVto.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Cb0psjSL.js → slide-gPQPrYZC.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Bjuo5Oal.js → statusUtils-DwWKX5co.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BLMhoHJ5.js → styleChecker-B3VOtXuH.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BdCwAfNU.js → useFlexGapSupport-6ADctM2r.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-9Jhaz5gG.js → useFs-6Zx1SSKs.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-xYFyXKwD.js → usePersonalDataHub-BzReowln.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CVhepE6Z.js → vnode-C8IpEQbD.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-IbbtJ4Zr.js → zoom-ruc9vHr0.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +161 -6
- package/src/commands/agents.js +199 -0
- package/src/commands/command.js +7 -2
- package/src/commands/hook.js +136 -28
- package/src/commands/ide.js +168 -0
- package/src/commands/mcp.js +92 -0
- package/src/commands/output-style.js +127 -0
- package/src/commands/permissions.js +211 -0
- package/src/commands/statusline.js +93 -0
- package/src/index.js +8 -0
- package/src/lib/agent-core.js +7 -0
- package/src/lib/agents.js +147 -0
- package/src/lib/hook-manager.js +1 -0
- package/src/lib/hook-runner.cjs +183 -0
- package/src/lib/ide-bridge.js +310 -0
- package/src/lib/image-input.js +156 -0
- package/src/lib/mcp-oauth.js +415 -0
- package/src/lib/output-styles.js +179 -0
- package/src/lib/permission-rules.cjs +325 -0
- package/src/lib/provider-options.js +11 -7
- package/src/lib/settings-hook-events.cjs +102 -0
- package/src/lib/settings-hooks.cjs +163 -0
- package/src/lib/settings-loader.cjs +244 -0
- package/src/lib/slash-commands.js +21 -13
- package/src/lib/status-line.cjs +204 -0
- package/src/lib/sub-agent-profiles.js +3 -0
- package/src/lib/web-search.js +487 -0
- package/src/repl/agent-repl.js +445 -35
- package/src/repl/slash-macro.js +45 -0
- package/src/runtime/agent-core.js +799 -21
- package/src/runtime/coding-agent-contract-shared.cjs +94 -4
- package/src/runtime/coding-agent-policy.cjs +24 -0
- package/src/runtime/headless-runner.js +162 -6
- package/src/runtime/headless-stream.js +133 -7
- package/src/runtime/mcp-config.js +161 -15
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/runtime/system-prompt.js +6 -1
- package/src/assets/web-panel/assets/MobileProjects-CUxONYre.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CExqxzL6.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DQYatsRR.js +0 -1
- package/src/assets/web-panel/assets/index-Bv_y1Ud7.js +0 -1
- package/src/assets/web-panel/assets/index-CZZnSJEX.js +0 -1
package/src/index.js
CHANGED
|
@@ -62,6 +62,10 @@ import { registerCheckpointCommand } from "./commands/checkpoint.js";
|
|
|
62
62
|
import { registerGoalCommand } from "./commands/goal.js";
|
|
63
63
|
import { registerCommandCommand } from "./commands/command.js";
|
|
64
64
|
import { registerCompactCommand } from "./commands/compact.js";
|
|
65
|
+
import { registerPermissionsCommand } from "./commands/permissions.js";
|
|
66
|
+
import { registerOutputStyleCommand } from "./commands/output-style.js";
|
|
67
|
+
import { registerStatuslineCommand } from "./commands/statusline.js";
|
|
68
|
+
import { registerIdeCommand } from "./commands/ide.js";
|
|
65
69
|
import { registerConsolCommand } from "./commands/consol.js";
|
|
66
70
|
import { registerImportCommand } from "./commands/import.js";
|
|
67
71
|
import { registerExportCommand } from "./commands/export.js";
|
|
@@ -460,6 +464,10 @@ export function createProgram(opts = {}) {
|
|
|
460
464
|
registerGoalCommand(program);
|
|
461
465
|
registerCommandCommand(program);
|
|
462
466
|
registerCompactCommand(program);
|
|
467
|
+
registerPermissionsCommand(program);
|
|
468
|
+
registerOutputStyleCommand(program);
|
|
469
|
+
registerStatuslineCommand(program);
|
|
470
|
+
registerIdeCommand(program);
|
|
463
471
|
registerConsolCommand(program);
|
|
464
472
|
|
|
465
473
|
// Phase 2: Knowledge & content management
|
package/src/lib/agent-core.js
CHANGED
|
@@ -24,5 +24,12 @@ export {
|
|
|
24
24
|
agentLoop,
|
|
25
25
|
formatToolArgs,
|
|
26
26
|
getActiveMcpServers,
|
|
27
|
+
listBackgroundShellTasks,
|
|
28
|
+
killAllBackgroundShellTasks,
|
|
27
29
|
_accumulateOllamaStream,
|
|
30
|
+
_accumulateOpenAIStream,
|
|
31
|
+
_accumulateAnthropicStream,
|
|
32
|
+
_toAnthropicMessages,
|
|
33
|
+
_anthropicThinkingParams,
|
|
34
|
+
_normalizeAnthropicResponse,
|
|
28
35
|
} from "../runtime/agent-core.js";
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents — user-defined subagent definitions (Claude-Code parity).
|
|
3
|
+
*
|
|
4
|
+
* Markdown files under `.claude/agents/` (project) or `~/.claude/agents/`
|
|
5
|
+
* (personal) define named subagents. Each file's body IS the subagent's system
|
|
6
|
+
* prompt; frontmatter declares its metadata. Mirrors `.claude/commands/` (see
|
|
7
|
+
* slash-commands.js) but for *agents* rather than prompt macros — a file
|
|
8
|
+
* `review/security.md` is the agent `review:security`.
|
|
9
|
+
*
|
|
10
|
+
* Frontmatter (all optional):
|
|
11
|
+
* name override the filename-derived name
|
|
12
|
+
* description one-line summary (when to use this agent)
|
|
13
|
+
* tools allow-list — comma string or YAML array; omit = inherit all
|
|
14
|
+
* model model override for runs of this agent
|
|
15
|
+
*
|
|
16
|
+
* Project scope shadows personal on a name clash. Discovery + parse are pure
|
|
17
|
+
* (inject fs/path/home) so the whole thing is unit-testable.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fsDefault from "node:fs";
|
|
21
|
+
import pathDefault from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import yaml from "js-yaml";
|
|
24
|
+
|
|
25
|
+
const _deps = { fs: fsDefault, path: pathDefault };
|
|
26
|
+
|
|
27
|
+
/** Split `--- ... ---` YAML frontmatter from the body, camelCasing keys. */
|
|
28
|
+
function parseFrontmatter(content) {
|
|
29
|
+
const text = String(content || "");
|
|
30
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
31
|
+
if (!m) return { data: {}, body: text.trim() };
|
|
32
|
+
let raw = {};
|
|
33
|
+
try {
|
|
34
|
+
raw = yaml.load(m[1]) || {};
|
|
35
|
+
} catch {
|
|
36
|
+
raw = {};
|
|
37
|
+
}
|
|
38
|
+
const data = {};
|
|
39
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
40
|
+
const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
41
|
+
data[camel] = v;
|
|
42
|
+
}
|
|
43
|
+
return { data, body: (m[2] || "").trim() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Normalize `tools` (comma string | array | null) into a string[] or null. */
|
|
47
|
+
export function normalizeTools(tools) {
|
|
48
|
+
if (tools == null) return null;
|
|
49
|
+
const list = Array.isArray(tools)
|
|
50
|
+
? tools
|
|
51
|
+
: String(tools).split(/[,\s]+/);
|
|
52
|
+
const out = list.map((t) => String(t).trim()).filter(Boolean);
|
|
53
|
+
return out.length > 0 ? out : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Directories scanned for agent files — project first (shadows personal). */
|
|
57
|
+
export function agentDirs(cwd = process.cwd(), opts = {}) {
|
|
58
|
+
const path = opts.deps?.path || _deps.path;
|
|
59
|
+
const home = opts.home || homedir();
|
|
60
|
+
// Project-native first (highest precedence), then the Claude-Code-portable
|
|
61
|
+
// location (so existing `.claude/agents/*.md` work unchanged), then personal.
|
|
62
|
+
// discoverAgents reverses + last-write-wins, so `.chainlesschain/agents/`
|
|
63
|
+
// shadows `.claude/agents/` shadows `~/.claude/agents/` on a name clash.
|
|
64
|
+
return [
|
|
65
|
+
{ dir: path.join(cwd, ".chainlesschain", "agents"), scope: "project" },
|
|
66
|
+
{ dir: path.join(cwd, ".claude", "agents"), scope: "project" },
|
|
67
|
+
{ dir: path.join(home, ".claude", "agents"), scope: "personal" },
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Recursively collect `*.md` files under `dir` as `{file, rel}` (rel uses /). */
|
|
72
|
+
function walkMd(dir, { fs, path }, base = dir, acc = []) {
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
} catch {
|
|
77
|
+
return acc;
|
|
78
|
+
}
|
|
79
|
+
for (const e of entries) {
|
|
80
|
+
const full = path.join(dir, e.name);
|
|
81
|
+
if (e.isDirectory()) {
|
|
82
|
+
walkMd(full, { fs, path }, base, acc);
|
|
83
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
84
|
+
const rel = path.relative(base, full).replace(/\\/g, "/");
|
|
85
|
+
acc.push({ file: full, rel });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return acc;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Agent name from a relative path: `review/security.md` → `review:security`. */
|
|
92
|
+
function nameFromRel(rel) {
|
|
93
|
+
return rel.replace(/\.md$/, "").replace(/\//g, ":");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Parse one agent file into its metadata + system prompt (the body). */
|
|
97
|
+
export function parseAgentFile(file, scope, opts = {}) {
|
|
98
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
99
|
+
let content;
|
|
100
|
+
try {
|
|
101
|
+
content = fs.readFileSync(file, "utf-8");
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const { data, body } = parseFrontmatter(content);
|
|
106
|
+
return {
|
|
107
|
+
file,
|
|
108
|
+
scope,
|
|
109
|
+
name: data.name || null, // resolved against the path in discoverAgents
|
|
110
|
+
description: data.description || "",
|
|
111
|
+
tools: normalizeTools(data.tools),
|
|
112
|
+
model: data.model || null,
|
|
113
|
+
systemPrompt: body || "",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Discover all agents across both scopes. Project shadows personal by name.
|
|
119
|
+
* @returns {Array<{name, scope, file, description, tools, model, systemPrompt}>}
|
|
120
|
+
*/
|
|
121
|
+
export function discoverAgents(cwd = process.cwd(), opts = {}) {
|
|
122
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
123
|
+
const path = opts.deps?.path || _deps.path;
|
|
124
|
+
const byName = new Map();
|
|
125
|
+
// Personal first, then project — so project overwrites on clash.
|
|
126
|
+
const dirs = agentDirs(cwd, opts).reverse();
|
|
127
|
+
for (const { dir, scope } of dirs) {
|
|
128
|
+
for (const { file, rel } of walkMd(dir, { fs, path })) {
|
|
129
|
+
const meta = parseAgentFile(file, scope, opts);
|
|
130
|
+
if (!meta) continue;
|
|
131
|
+
// Explicit frontmatter `name` wins; else derive from the path.
|
|
132
|
+
const name = meta.name || nameFromRel(rel);
|
|
133
|
+
byName.set(name, { ...meta, name });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Look up one agent by name (accepts `review:security` or `review/security`). */
|
|
140
|
+
export function getAgent(name, cwd = process.cwd(), opts = {}) {
|
|
141
|
+
const wanted = String(name || "")
|
|
142
|
+
.replace(/^\//, "")
|
|
143
|
+
.replace(/\//g, ":");
|
|
144
|
+
return discoverAgents(cwd, opts).find((a) => a.name === wanted) || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { _deps };
|
package/src/lib/hook-manager.js
CHANGED
|
@@ -40,6 +40,7 @@ export const HookEvents = {
|
|
|
40
40
|
SessionEnd: "SessionEnd",
|
|
41
41
|
PreCompact: "PreCompact",
|
|
42
42
|
PostCompact: "PostCompact",
|
|
43
|
+
Notification: "Notification",
|
|
43
44
|
UserPromptSubmit: "UserPromptSubmit",
|
|
44
45
|
AssistantResponse: "AssistantResponse",
|
|
45
46
|
AgentStart: "AgentStart",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hook-runner — execute a Claude-Code `command` hook with the JSON protocol.
|
|
5
|
+
*
|
|
6
|
+
* Protocol (Claude-Code parity):
|
|
7
|
+
* - the hook event payload is written to the command's STDIN as JSON;
|
|
8
|
+
* - exit code 2 → BLOCK (reason = stderr) — the canonical "deny" path;
|
|
9
|
+
* - exit code 0 + JSON stdout → honored decision:
|
|
10
|
+
* { "decision": "block"|"approve"|"ask", "reason": "..." }
|
|
11
|
+
* { "hookSpecificOutput": { "permissionDecision": "deny"|"allow"|"ask",
|
|
12
|
+
* "permissionDecisionReason": "..." } }
|
|
13
|
+
* { "continue": false, "stopReason": "..." } → block
|
|
14
|
+
* { "additionalContext": "..." } → continue
|
|
15
|
+
* - any other non-zero → non-blocking error (surfaced, never blocks);
|
|
16
|
+
* - spawn failure / timeout → non-blocking (a broken hook must not wedge the
|
|
17
|
+
* agent — only an explicit block decision blocks).
|
|
18
|
+
*
|
|
19
|
+
* Returns a normalized `{ decision, reason, exitCode, stdout, stderr, ... }`.
|
|
20
|
+
* `_deps.spawnSync` is injected for unit tests (no real process needed).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const cpDefault = require("node:child_process");
|
|
24
|
+
|
|
25
|
+
const _deps = { spawnSync: cpDefault.spawnSync };
|
|
26
|
+
|
|
27
|
+
const HOOK_DECISIONS = Object.freeze({
|
|
28
|
+
BLOCK: "block",
|
|
29
|
+
ALLOW: "allow",
|
|
30
|
+
ASK: "ask",
|
|
31
|
+
CONTINUE: "continue",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Parse a hook's stdout JSON into a normalized decision, or null if not JSON. */
|
|
35
|
+
function tryParseDecision(stdout) {
|
|
36
|
+
const text = String(stdout || "").trim();
|
|
37
|
+
if (!text || text[0] !== "{") return null;
|
|
38
|
+
let obj;
|
|
39
|
+
try {
|
|
40
|
+
obj = JSON.parse(text);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// PreToolUse-specific permission decision
|
|
45
|
+
const hso = obj.hookSpecificOutput;
|
|
46
|
+
if (hso && hso.permissionDecision) {
|
|
47
|
+
const pd = String(hso.permissionDecision).toLowerCase();
|
|
48
|
+
const decision =
|
|
49
|
+
pd === "deny"
|
|
50
|
+
? HOOK_DECISIONS.BLOCK
|
|
51
|
+
: pd === "ask"
|
|
52
|
+
? HOOK_DECISIONS.ASK
|
|
53
|
+
: pd === "allow"
|
|
54
|
+
? HOOK_DECISIONS.ALLOW
|
|
55
|
+
: HOOK_DECISIONS.CONTINUE;
|
|
56
|
+
return { decision, reason: hso.permissionDecisionReason || null };
|
|
57
|
+
}
|
|
58
|
+
// Generic decision field
|
|
59
|
+
if (obj.decision) {
|
|
60
|
+
const d = String(obj.decision).toLowerCase();
|
|
61
|
+
const decision =
|
|
62
|
+
d === "block" || d === "deny"
|
|
63
|
+
? HOOK_DECISIONS.BLOCK
|
|
64
|
+
: d === "approve" || d === "allow"
|
|
65
|
+
? HOOK_DECISIONS.ALLOW
|
|
66
|
+
: d === "ask"
|
|
67
|
+
? HOOK_DECISIONS.ASK
|
|
68
|
+
: HOOK_DECISIONS.CONTINUE;
|
|
69
|
+
return { decision, reason: obj.reason || null };
|
|
70
|
+
}
|
|
71
|
+
// continue:false → stop/block
|
|
72
|
+
if (obj.continue === false) {
|
|
73
|
+
return {
|
|
74
|
+
decision: HOOK_DECISIONS.BLOCK,
|
|
75
|
+
reason: obj.stopReason || obj.reason || "hook requested stop",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
80
|
+
reason: null,
|
|
81
|
+
additionalContext: obj.additionalContext || null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run one command hook. `input` is JSON-serialized to the hook's stdin.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} command
|
|
89
|
+
* @param {object} [input] the hook event payload (tool_name, tool_input, …)
|
|
90
|
+
* @param {object} [opts] { timeout=60000, cwd, event }
|
|
91
|
+
* @returns {{ decision:string, reason:string|null, exitCode:number|null,
|
|
92
|
+
* stdout?:string, stderr?:string, additionalContext?:string,
|
|
93
|
+
* nonBlockingError?:boolean, error?:string }}
|
|
94
|
+
*/
|
|
95
|
+
function runCommandHook(command, input = {}, opts = {}) {
|
|
96
|
+
const { timeout = 60000, cwd, event } = opts;
|
|
97
|
+
if (!command) {
|
|
98
|
+
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
|
|
99
|
+
}
|
|
100
|
+
let res;
|
|
101
|
+
try {
|
|
102
|
+
res = _deps.spawnSync(command, {
|
|
103
|
+
input: JSON.stringify(input),
|
|
104
|
+
cwd: cwd || process.cwd(),
|
|
105
|
+
encoding: "utf-8",
|
|
106
|
+
timeout,
|
|
107
|
+
shell: true,
|
|
108
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
CLAUDE_HOOK_EVENT: event || input.hook_event_name || "",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
117
|
+
reason: `hook spawn failed: ${err.message}`,
|
|
118
|
+
exitCode: null,
|
|
119
|
+
error: err.message,
|
|
120
|
+
nonBlockingError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// spawnSync surfaces timeout/ENOENT on res.error (status null) — non-blocking.
|
|
124
|
+
if (res.error) {
|
|
125
|
+
return {
|
|
126
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
127
|
+
reason: `hook error: ${res.error.message}`,
|
|
128
|
+
exitCode: null,
|
|
129
|
+
stdout: String(res.stdout || ""),
|
|
130
|
+
stderr: String(res.stderr || ""),
|
|
131
|
+
error: res.error.message,
|
|
132
|
+
nonBlockingError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const exitCode = res.status;
|
|
136
|
+
const stdout = String(res.stdout || "");
|
|
137
|
+
const stderr = String(res.stderr || "");
|
|
138
|
+
|
|
139
|
+
if (exitCode === 2) {
|
|
140
|
+
return {
|
|
141
|
+
decision: HOOK_DECISIONS.BLOCK,
|
|
142
|
+
reason: stderr.trim() || "hook exited 2 (blocked)",
|
|
143
|
+
exitCode,
|
|
144
|
+
stdout,
|
|
145
|
+
stderr,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (exitCode === 0) {
|
|
149
|
+
const parsed = tryParseDecision(stdout);
|
|
150
|
+
if (parsed) return { ...parsed, exitCode, stdout, stderr };
|
|
151
|
+
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode, stdout, stderr };
|
|
152
|
+
}
|
|
153
|
+
// Other non-zero → non-blocking error.
|
|
154
|
+
return {
|
|
155
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
156
|
+
reason: stderr.trim() || `hook exited ${exitCode}`,
|
|
157
|
+
exitCode,
|
|
158
|
+
stdout,
|
|
159
|
+
stderr,
|
|
160
|
+
nonBlockingError: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run a list of command hooks in order; first BLOCK (or ASK) short-circuits.
|
|
166
|
+
* @returns {{ decision, reason, hook?, results }}
|
|
167
|
+
*/
|
|
168
|
+
function runHooks(commandHooks, input = {}, opts = {}) {
|
|
169
|
+
const results = [];
|
|
170
|
+
for (const h of commandHooks || []) {
|
|
171
|
+
const r = runCommandHook(h.command, input, {
|
|
172
|
+
...opts,
|
|
173
|
+
timeout: h.timeout != null ? h.timeout * 1000 : opts.timeout,
|
|
174
|
+
});
|
|
175
|
+
results.push({ command: h.command, ...r });
|
|
176
|
+
if (r.decision === HOOK_DECISIONS.BLOCK || r.decision === HOOK_DECISIONS.ASK) {
|
|
177
|
+
return { decision: r.decision, reason: r.reason, hook: h.command, results };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, results };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { runCommandHook, runHooks, tryParseDecision, HOOK_DECISIONS, _deps };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE bridge discovery (Phase 0) — find a first-party IDE MCP server that an
|
|
3
|
+
* editor extension advertises via a lockfile, and turn it into an MCP server
|
|
4
|
+
* config the agent loop can connect to.
|
|
5
|
+
*
|
|
6
|
+
* Protocol (design: docs/design/modules/98_IDE桥接对标方案.md): each running
|
|
7
|
+
* IDE instance writes
|
|
8
|
+
* ~/.chainlesschain/ide/<port>.json (file 0600, dir 0700)
|
|
9
|
+
* describing a localhost SSE/HTTP MCP server + a per-instance bearer token.
|
|
10
|
+
* The extension that owns the integrated terminal ALSO injects
|
|
11
|
+
* CHAINLESSCHAIN_IDE_PORT (+ optional CHAINLESSCHAIN_IDE_TOKEN)
|
|
12
|
+
* so the CLI can lock onto the exact instance without scanning/guessing
|
|
13
|
+
* (the deterministic "path A" below). Plain terminals fall back to a lockfile
|
|
14
|
+
* scan + workspace match ("path B").
|
|
15
|
+
*
|
|
16
|
+
* Phase 0 is pure CLI: no extension code, no VS Code dependency, fully
|
|
17
|
+
* unit-testable through `_deps`. The connect path itself reuses the existing
|
|
18
|
+
* MCP client (SSE/HTTP) via mcp-config.js `loadIdeMcp`.
|
|
19
|
+
*/
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import os from "os";
|
|
23
|
+
|
|
24
|
+
/** A lock whose pid is dead and whose file is older than this = stale. */
|
|
25
|
+
const STALE_TTL_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
/** Transports the CLI's MCP client can actually talk (see mcp-client.js). */
|
|
28
|
+
const SUPPORTED_TRANSPORTS = new Set(["sse", "http", "https"]);
|
|
29
|
+
|
|
30
|
+
/** Liveness probe via signal 0 — cross-platform (works on Windows too). */
|
|
31
|
+
function defaultProcessAlive(pid) {
|
|
32
|
+
if (!pid || typeof pid !== "number") return false;
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// EPERM: the process exists but we may not signal it → still alive.
|
|
38
|
+
return !!(err && err.code === "EPERM");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const _deps = {
|
|
43
|
+
readDir: (d) => fs.readdirSync(d),
|
|
44
|
+
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
45
|
+
statMtimeMs: (p) => fs.statSync(p).mtimeMs,
|
|
46
|
+
homedir: () => os.homedir(),
|
|
47
|
+
now: () => Date.now(),
|
|
48
|
+
processAlive: defaultProcessAlive,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** `~/.chainlesschain/ide/`. */
|
|
52
|
+
export function ideLockDir(deps = _deps) {
|
|
53
|
+
return path.join(deps.homedir(), ".chainlesschain", "ide");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isLocalhostUrl(url) {
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(url);
|
|
59
|
+
return u.hostname === "127.0.0.1" || u.hostname === "::1";
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function inferTransportFromUrl(url) {
|
|
66
|
+
if (/\/sse(\b|\/|$)/i.test(url)) return "sse";
|
|
67
|
+
if (/^https:/i.test(url)) return "https";
|
|
68
|
+
if (/^http:/i.test(url)) return "http";
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Parse + validate one lock JSON. Returns a normalized lock or null. */
|
|
73
|
+
function parseLock(raw, filePath) {
|
|
74
|
+
let lock;
|
|
75
|
+
try {
|
|
76
|
+
lock = JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!lock || typeof lock !== "object") return null;
|
|
81
|
+
if (!lock.url || !isLocalhostUrl(lock.url)) return null;
|
|
82
|
+
|
|
83
|
+
const transport = String(
|
|
84
|
+
lock.transport || inferTransportFromUrl(lock.url) || "",
|
|
85
|
+
).toLowerCase();
|
|
86
|
+
if (!SUPPORTED_TRANSPORTS.has(transport)) return null; // e.g. ws — not yet
|
|
87
|
+
|
|
88
|
+
let folders = lock.workspaceFolders;
|
|
89
|
+
if (typeof folders === "string") folders = [folders];
|
|
90
|
+
if (!Array.isArray(folders)) folders = [];
|
|
91
|
+
folders = folders.filter((f) => typeof f === "string" && f.length > 0);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
ide: typeof lock.ide === "string" ? lock.ide : "unknown",
|
|
95
|
+
transport,
|
|
96
|
+
url: lock.url,
|
|
97
|
+
port: lock.port,
|
|
98
|
+
token: typeof lock.token === "string" ? lock.token : null,
|
|
99
|
+
workspaceFolders: folders,
|
|
100
|
+
pid: typeof lock.pid === "number" ? lock.pid : null,
|
|
101
|
+
startedAt: typeof lock.started_at === "number" ? lock.started_at : 0,
|
|
102
|
+
_file: filePath,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** True when a lock is dead: pid gone AND file older than the TTL. */
|
|
107
|
+
function isStale(lock, deps) {
|
|
108
|
+
if (lock.pid && deps.processAlive(lock.pid)) return false;
|
|
109
|
+
let mtime;
|
|
110
|
+
try {
|
|
111
|
+
mtime = deps.statMtimeMs(lock._file);
|
|
112
|
+
} catch {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return deps.now() - mtime > STALE_TTL_MS;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scan the lock dir → normalized, live, localhost, supported-transport locks.
|
|
120
|
+
* Missing dir / unreadable files are silently skipped (best-effort).
|
|
121
|
+
*/
|
|
122
|
+
export function readIdeLocks(deps = _deps) {
|
|
123
|
+
const dir = ideLockDir(deps);
|
|
124
|
+
let names;
|
|
125
|
+
try {
|
|
126
|
+
names = deps.readDir(dir);
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const name of names || []) {
|
|
132
|
+
if (!String(name).endsWith(".json")) continue;
|
|
133
|
+
const fp = path.join(dir, name);
|
|
134
|
+
let raw;
|
|
135
|
+
try {
|
|
136
|
+
raw = deps.readFile(fp);
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const lock = parseLock(raw, fp);
|
|
141
|
+
if (!lock) continue;
|
|
142
|
+
if (isStale(lock, deps)) continue;
|
|
143
|
+
out.push(lock);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Are we running inside an editor's integrated terminal? */
|
|
149
|
+
export function isInIdeTerminal(env = process.env) {
|
|
150
|
+
if (!env) return false;
|
|
151
|
+
if (env.CHAINLESSCHAIN_IDE_PORT) return true;
|
|
152
|
+
if (env.TERM_PROGRAM === "vscode") return true;
|
|
153
|
+
if (env.CHAINLESSCHAIN_IDE) return true;
|
|
154
|
+
if (env.TERMINAL_EMULATOR && /jetbrains/i.test(env.TERMINAL_EMULATOR)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Normalize a path for prefix comparison (sep + trailing slash + win case). */
|
|
161
|
+
function normPath(p) {
|
|
162
|
+
const s = String(p)
|
|
163
|
+
.replace(/\\/g, "/")
|
|
164
|
+
.replace(/\/+$/, "");
|
|
165
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Longest matching workspace-folder length for `cwd`, or -1 if none of the
|
|
170
|
+
* lock's folders contains (or equals) cwd. Longer = more specific.
|
|
171
|
+
*/
|
|
172
|
+
function workspaceMatchScore(lock, cwd) {
|
|
173
|
+
const c = normPath(cwd);
|
|
174
|
+
let best = -1;
|
|
175
|
+
for (const f of lock.workspaceFolders) {
|
|
176
|
+
const nf = normPath(f);
|
|
177
|
+
if (c === nf || c.startsWith(nf + "/")) {
|
|
178
|
+
if (nf.length > best) best = nf.length;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return best;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Pick the IDE server to connect to, or null.
|
|
186
|
+
*
|
|
187
|
+
* Path A (deterministic): if `CHAINLESSCHAIN_IDE_PORT` is set and a live lock
|
|
188
|
+
* has that port, use it (token may come from the lock or the env).
|
|
189
|
+
* Path B (scan): otherwise rank live locks by (longest workspace-prefix match,
|
|
190
|
+
* then newest started_at). With `force`, fall back to the newest lock even
|
|
191
|
+
* when no folder matches (used by `--ide`).
|
|
192
|
+
*
|
|
193
|
+
* @param {object} opts { cwd?, env?, force? }
|
|
194
|
+
* @param {object} [deps]
|
|
195
|
+
*/
|
|
196
|
+
export function discoverIdeServer(
|
|
197
|
+
{ cwd = process.cwd(), env = process.env, force = false } = {},
|
|
198
|
+
deps = _deps,
|
|
199
|
+
) {
|
|
200
|
+
const locks = readIdeLocks(deps);
|
|
201
|
+
if (!locks.length) return null;
|
|
202
|
+
|
|
203
|
+
// Path A — env fast-path.
|
|
204
|
+
const envPort = env && env.CHAINLESSCHAIN_IDE_PORT;
|
|
205
|
+
if (envPort) {
|
|
206
|
+
const hit = locks.find((l) => String(l.port) === String(envPort));
|
|
207
|
+
if (hit) {
|
|
208
|
+
if (!hit.token && env.CHAINLESSCHAIN_IDE_TOKEN) {
|
|
209
|
+
hit.token = env.CHAINLESSCHAIN_IDE_TOKEN;
|
|
210
|
+
}
|
|
211
|
+
return hit;
|
|
212
|
+
}
|
|
213
|
+
// env named a port with no live lock — fall through to scan.
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Path B — workspace match.
|
|
217
|
+
let best = null;
|
|
218
|
+
let bestScore = -1;
|
|
219
|
+
let bestStarted = -1;
|
|
220
|
+
for (const l of locks) {
|
|
221
|
+
const score = workspaceMatchScore(l, cwd);
|
|
222
|
+
if (score < 0) continue;
|
|
223
|
+
if (
|
|
224
|
+
score > bestScore ||
|
|
225
|
+
(score === bestScore && l.startedAt > bestStarted)
|
|
226
|
+
) {
|
|
227
|
+
best = l;
|
|
228
|
+
bestScore = score;
|
|
229
|
+
bestStarted = l.startedAt;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (best) return best;
|
|
233
|
+
|
|
234
|
+
// --ide forced but no workspace match → newest live lock.
|
|
235
|
+
if (force) {
|
|
236
|
+
return locks
|
|
237
|
+
.slice()
|
|
238
|
+
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* A discovered lock → an MCP server config row for `setupMcpFromConfig`.
|
|
245
|
+
* `longRunning` is forward-compat metadata: IDE tools like `ide_openDiff` may
|
|
246
|
+
* block on the user for minutes, so the agent loop should exempt them from a
|
|
247
|
+
* per-call timeout (consumed in Phase 1).
|
|
248
|
+
*/
|
|
249
|
+
export function ideServerToMcpConfig(lock) {
|
|
250
|
+
if (!lock || !lock.url) return null;
|
|
251
|
+
const headers = {};
|
|
252
|
+
if (lock.token) headers.Authorization = `Bearer ${lock.token}`;
|
|
253
|
+
return {
|
|
254
|
+
url: lock.url,
|
|
255
|
+
transport: lock.transport,
|
|
256
|
+
headers,
|
|
257
|
+
longRunning: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Human-readable explanation of why discovery did/didn't find a server — backs
|
|
263
|
+
* `cc ide doctor`. Returns { inIdeTerminal, locks:[{...,reasons?}], chosen,
|
|
264
|
+
* reason }.
|
|
265
|
+
*/
|
|
266
|
+
export function diagnoseIde(
|
|
267
|
+
{ cwd = process.cwd(), env = process.env, force = false } = {},
|
|
268
|
+
deps = _deps,
|
|
269
|
+
) {
|
|
270
|
+
const inIdeTerminal = isInIdeTerminal(env);
|
|
271
|
+
const locks = readIdeLocks(deps);
|
|
272
|
+
const chosen = discoverIdeServer({ cwd, env, force }, deps);
|
|
273
|
+
|
|
274
|
+
let reason;
|
|
275
|
+
if (chosen) {
|
|
276
|
+
reason = env && env.CHAINLESSCHAIN_IDE_PORT &&
|
|
277
|
+
String(chosen.port) === String(env.CHAINLESSCHAIN_IDE_PORT)
|
|
278
|
+
? "matched CHAINLESSCHAIN_IDE_PORT (env fast-path)"
|
|
279
|
+
: force
|
|
280
|
+
? "forced (--ide); newest live lock"
|
|
281
|
+
: "workspace match";
|
|
282
|
+
} else if (!locks.length) {
|
|
283
|
+
reason =
|
|
284
|
+
"no live IDE lockfiles in " +
|
|
285
|
+
ideLockDir(deps) +
|
|
286
|
+
" (is an IDE extension running? are locks stale?)";
|
|
287
|
+
} else {
|
|
288
|
+
reason =
|
|
289
|
+
"lockfiles present but none match cwd's workspace " +
|
|
290
|
+
"(run with --ide to force the newest, or cd into the IDE workspace)";
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
inIdeTerminal,
|
|
294
|
+
lockDir: ideLockDir(deps),
|
|
295
|
+
locks: locks.map((l) => ({
|
|
296
|
+
ide: l.ide,
|
|
297
|
+
port: l.port,
|
|
298
|
+
transport: l.transport,
|
|
299
|
+
url: l.url,
|
|
300
|
+
hasToken: !!l.token,
|
|
301
|
+
workspaceFolders: l.workspaceFolders,
|
|
302
|
+
pid: l.pid,
|
|
303
|
+
matchScore: workspaceMatchScore(l, cwd),
|
|
304
|
+
})),
|
|
305
|
+
chosen: chosen
|
|
306
|
+
? { ide: chosen.ide, port: chosen.port, url: chosen.url }
|
|
307
|
+
: null,
|
|
308
|
+
reason,
|
|
309
|
+
};
|
|
310
|
+
}
|