{"version":3,"sources":["jsdelivr-header.js","/npm/codebuddy-first-bridge@1.6.2/home-plugin/codebuddy-indicator/lib/index.mjs"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,OCkCS,gCAAmC,iCAM1C,SAAU,cAAe,uBAAwB,wBAAyB,uBAC1E,sBAAuB,uBAAwB,gBAAiB,uBAC3D,0CAEA,MAAM,cAAgB,YACtB,MAAM,UAAY,yBAOlB,MAAM,gBAAkB,sCACxB,MAAM,oBAAsB,IAGnC,MAAM,aAAe,UAId,SAAS,mBAAmB,EAAS,EAAO,GACjD,IAAK,GAA8B,iBAAZ,EAAsB,OAAO,KACpD,MAAM,EAAW,MAAM,QAAQ,EAAQ,UAAY,EAAQ,SAAW,GAChE,EAAY,OAAO,EAAQ,YAAc,EACzC,GAAS,OAAO,IAAU,GAAK,GAAa,OAAO,IAXxB,KAY3B,EAAM,GACZ,IAAK,MAAM,KAAK,EACT,GAAM,EAAE,MACT,IAAU,OAAO,EAAE,UAAY,GAAK,EACtC,EAAI,KAAK,OAAO,OAAO,CAAC,EAAG,EAAG,CAAE,QAAS,EAAG,QAAS,KAAM,MAAmB,YAAZ,EAAE,MAAsB,OAAS,EAAE,SAChG,EAAI,KAAK,IAElB,MAAO,CAAE,OAAQ,MAAO,UAAW,EAAW,MAAO,EAAO,SAAU,EACxE,QAIO,SAAS,qBAAqB,GACnC,MAAM,EAAI,GAAQ,CAAC,EACb,EAAuB,mBAAV,EAAE,IAAqB,EAAE,IAAM,KAAK,IACjD,EAAmB,MAAX,EAAE,MAAgB,EAAE,MApCP,KAqCrB,EAAqD,mBAAzB,EAAE,mBAAoC,EAAE,mBAAqB,KACzF,EAAW,OAAO,OAAO,MAE/B,IAAI,EAAoB,EACpB,EAAa,EAEjB,MAAM,EAAgB,OAAO,OAAO,MAEpC,SAAS,EAAY,GACnB,MAAM,EAAI,OAAO,GAAO,IAClB,EAAQ,EAAE,MAAM,SAAS,OAAO,SACtC,OAAO,EAAM,OAAS,EAAM,EAAM,OAAS,GAAK,CAClD,CAuGA,MAAO,CAAE,OAnGT,SAAgB,GACd,IACE,MAAM,EAAM,GAAwC,iBAAtB,EAAQ,WAA0B,EAAQ,UAAY,EAAQ,UAAY,KACxG,GAAI,GAAW,EAAQ,QAGrB,GAFA,EAAoB,IAAQ,EAC5B,EAAa,IACT,EAAK,CACP,EAAc,GAAO,IAAQ,EAE7B,MAAM,EAAM,OAAO,KAAK,GACxB,GAAI,EAAI,OAnDG,GAmDoB,CAC7B,EAAI,KAAK,CAAC,EAAG,IAAM,EAAc,GAAK,EAAc,IACpD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAI,OArDf,GAqDsC,WAAY,EAAc,EAAI,GAC/E,CACF,OAKI,UACK,EAAc,GACqB,IAAtC,OAAO,KAAK,GAAe,SAAc,EAAoB,IAEjE,EAAoB,CAG1B,CAAE,MAAO,GAAK,CAChB,EAwEiB,cAtEjB,SAAuB,GACrB,IAAK,GAAwB,iBAAT,EAAmB,OACvC,MAAM,EAAO,MAAM,QAAQ,EAAK,UAAY,EAAK,SAAY,EAAK,IAAM,CAAC,GAAQ,GACjF,IAAK,MAAM,KAAK,EACT,GAAM,EAAE,MACb,EAAS,EAAE,KAAO,CAChB,IAAK,EAAE,IACP,KAAM,EAAE,MAAQ,EAAY,EAAE,KAC9B,MAAO,EAAE,OAAS,OAClB,QAAS,OAAO,EAAE,UAAY,EAC9B,QAAS,EAAE,SAAW,KACtB,MAAO,MAAM,QAAQ,EAAE,OAAS,EAAE,MAAM,OAAO,IAAM,GACrD,WAAY,EAAE,YAAc,KAC5B,OAAQ,EAAE,QAAU,EACpB,cAAe,EAAE,eAAiB,KAClC,iBAAkB,EAAE,eACpB,UAAW,OAAO,EAAE,YAAc,KAAK,QAG3C,MAAM,EAAO,OAAO,KAAK,GACzB,GAAI,EAAK,OA/DU,GA+Da,CAC9B,MAAM,EAAQ,EACX,IAAK,GAAM,EAAS,IACpB,OAAQ,GAAoB,IAAd,EAAE,UAAkB,EAAE,gBACpC,KAAK,CAAC,EAAG,IAAM,EAAE,UAAY,EAAE,WAC5B,EAAO,KAAK,IAAI,EAAG,EAAK,OApEb,IAqEjB,IAAK,IAAI,EAAI,EAAG,EAAI,GAAQ,EAAI,EAAM,OAAQ,WAAY,EAAS,EAAM,GAAG,IAC9E,CACF,EA0CgC,SAvChC,WACE,MAAM,EAAI,IAEJ,EAAO,OAAO,OAAO,MACrB,EAAiB,GACvB,GAAI,EACF,IACE,MAAM,EAAO,IACb,GAAI,MAAM,QAAQ,GAChB,IAAK,MAAM,KAAO,EACG,iBAAR,GAAoB,IAAQ,EAAK,KAAQ,EAAK,GAAO,EAAG,EAAe,KAAK,GAG7F,CAAE,MAAO,GAAK,CAGhB,IAAK,MAAM,KAAO,OAAO,KAAK,GACxB,EAAI,EAAc,GACf,EAAK,KAAQ,EAAK,GAAO,EAAG,EAAe,KAAK,WACzC,EAAc,GAE9B,MAAM,EAAe,EAAI,GAAqB,EAAe,OAAS,EAChE,EAAa,EAAe,IAAiB,IAE7C,EAAO,OAAO,KAAK,GACtB,IAAK,GAAM,EAAS,IACpB,OAAQ,IACP,MAAM,EAAM,GAAK,OAAO,EAAE,YAAc,GACxC,SAAI,EAAE,QAAU,GAAK,EAAE,kBACP,YAAZ,EAAE,QACU,OAAZ,EAAE,OAA8B,WAAZ,EAAE,MAA2B,EAAM,EACpD,EARM,QAUd,KAAK,CAAC,EAAG,IAAM,EAAE,UAAY,EAAE,WAC5B,EAAU,EAAK,OAAO,CAAC,EAAG,IAAM,EAAI,EAAE,QAAS,GAErD,MAAO,CAAE,MADK,EAAU,EAAI,UAAa,EAAK,KAAM,GAAM,EAAE,gBAAkB,WAAc,EAAK,OAAS,KAAO,OACjG,UAAS,SAAU,EAAM,eAAc,iBAAgB,aACzE,EAGF,QAEO,MAAM,KAAO,6BACb,MAAM,OAAS,UAMf,SAAS,UAAU,GACxB,MAAM,EAAI,OAAO,GAAO,IACxB,OAAK,EACE,CAAE,KAAK,EAAM,KAAM,EAAE,QAAU,EAAI,OAAS,OAAS,EAAE,OAAO,IADtD,CAAE,KAAK,EAAO,KAAM,GAErC,QAMO,SAAS,aAAa,GAC3B,MAAM,EAAI,GAAM,CAAC,EACX,EAAW,uBAAuB,GAClC,EAAO,GAAY,wBACzB,MAAO,CACL,SAAU,mBACV,UAAwB,OAAb,EACX,iBAAkB,EAAK,iBACvB,aAAc,EAAK,aACnB,iBAAkB,EAAK,iBAEvB,iBAAkB,UAAU,EAAK,kBACjC,SAAU,uBAAuB,GACjC,YAAa,SAAS,IAAK,GAAM,gBAAgB,EAAG,EAAM,IAE9D,QAGO,SAAS,kBAAkB,GAChC,GAAoB,iBAAT,IAAsB,EAAK,OAAQ,MAAO,CAAE,IAAI,EAAO,MAAO,cACzE,IAAI,EACJ,IAAM,EAAM,KAAK,MAAM,EAAM,CAAE,MAAO,GAAK,MAAO,CAAE,IAAI,EAAO,MAAO,eAAiB,CACvF,IAAK,GAAsB,iBAAR,GAAoB,MAAM,QAAQ,GAAM,MAAO,CAAE,IAAI,EAAO,MAAO,0BACtF,IAAK,MAAM,KAAK,cAAe,CAC7B,MAAM,EAAI,EAAI,GACd,QAAU,IAAN,GAAgC,iBAAN,EAAgB,MAAO,CAAE,IAAI,EAAO,MAAO,SAAW,EAAI,oBAC1F,CACA,MAAO,CAAE,IAAI,EAAM,IAAK,EAAK,MAAO,uBAAuB,GAC7D,QAEO,SAAS,MAAM,GAsBpB,MAAM,EAAK,qBAAqB,CAAE,mBAnBlC,WACE,IACE,MAAM,EAA4B,mBAAZ,EAAI,IAAqB,EAAI,IAAI,eAAY,EAC7D,EAA6B,mBAAZ,EAAI,IAAqB,EAAI,IAAI,qBAAkB,EAC1E,IAAK,GAAiC,mBAAhB,EAAO,KAAqB,MAAO,GACzD,IAAK,GAA6C,mBAA3B,EAAQ,eAA+B,MAAO,GACrE,MAAM,EAAM,GACZ,IAAK,MAAM,KAAS,EAAO,OACzB,IACE,IAAK,IAAU,EAAM,IAAK,SAC1B,GAAI,EAAQ,eAAe,EAAM,OAAS,UAAW,SACrD,MAAM,EAAM,EAAM,IAAO,EAAM,SAAW,EAAM,QAAQ,GACrC,iBAAR,GAAoB,GAAK,EAAI,KAAK,EAC/C,CAAE,MAAO,GAAK,CAEhB,OAAO,CACT,CAAE,MAAO,GAAK,MAAO,EAAG,CAC1B,IAIA,EAAI,GAAG,iBAAkB,EAAG,QAE5B,EAAI,GAAG,mBAAqB,IAC1B,IACE,MAAM,EAAO,GAA8B,iBAAZ,GAAwB,EAAQ,SAAW,EAAQ,SAAW,EAC7F,EAAG,cAAc,EACnB,CAAE,MAAO,GAAK,IAGhB,IACE,EAAI,QAAQ,qBAAsB,CAAE,cAAe,EAAG,eACxD,CAAE,MAAO,GAAK,CAKd,SAAS,EAAQ,GACf,IACE,OAAQ,WAAW,SAA0D,mBAAxC,WAAW,QAAQ,iBACpD,WAAW,QAAQ,iBAAiB,GACpC,IACN,CAAE,MAAO,GAAK,OAAO,IAAK,CAC5B,CACA,MAAM,EAAS,EAAQ,WACjB,EAAW,EAAQ,aACnB,EAAU,EAAQ,YAExB,IAAI,EAAa,KACjB,IACE,MAAM,EAAS,WAAW,SAAW,WAAW,QAAQ,IACnD,WAAW,QAAQ,IAAI,yBAA2B,WAAW,QAAQ,IAAI,SAC1E,KACJ,GAAI,GAAU,EACZ,EAAa,EAAS,KAAK,EAAQ,sBAC9B,GAAI,GAAY,eAAuB,IAAK,CACjD,MAAM,EAAS,EAAS,QAAQ,EAAQ,0BAA0B,MAC5D,EAAO,EAAS,QAAQ,EAAQ,KAAM,KAAM,MAClD,EAAa,EAAS,KAAK,EAAM,gBACnC,CACF,CAAE,MAAO,GAAK,EAAa,IAAK,CAEhC,IAAI,EAAkB,EAmBtB,IACE,4BAA4B,EAC9B,CAAE,MAAO,GAA8B,CAEb,mBAAf,EAAI,QACb,EAAI,OAAO,CAAC,aAAe,IACzB,MAAM,EAAK,EAAO,IAAI,aACtB,IAAK,GAA6B,mBAAhB,EAAG,SAAyB,OAC9C,EAAO,OAAO,IAAM,EAAG,SAAS,CAC9B,KAAM,QACN,KAAM,8BACN,QAAS,CAAC,EAAK,KACb,KA9BR,WACE,GAAK,GAAe,EACpB,IACE,MAAM,EAAQ,OAAO,EAAO,SAAS,GAAY,UAAY,EAG7D,GAAI,IAAU,EAAiB,OAC/B,MAAM,EAAO,mBAAmB,KAAK,MAAM,EAAO,aAAa,EAAY,SAAU,KAAK,MA1Q7D,KA2QzB,GAAQ,EAAK,SAAS,QAAQ,EAAG,cAAc,GACnD,EAAkB,CACpB,CAAE,MAAO,GAAK,CAChB,CAoBU,GACA,MAAM,EAAO,KAAK,UAAU,EAAG,YAC/B,EAAI,UAAU,IAAK,CAAE,eAAgB,mBAAoB,gBAAiB,aAC1E,EAAI,IAAI,EACV,CAAE,MAAO,GACP,EAAI,UAAU,IAAK,CAAE,eAAgB,qBACrC,EAAI,IAAI,KAAK,UAAU,CAAE,MAAO,OAAO,GAAK,EAAE,SAAW,KAC3D,KAEA,qCAOJ,MAYM,EAAU,CAAC,EAAK,EAAM,KAC1B,IACE,EAAI,UAAU,EAAM,CAAE,eAAgB,mBAAoB,gBAAiB,aAC3E,EAAI,IAAI,KAAK,UAAU,GACzB,CAAE,MAAO,GAAsB,GAGjC,EAAO,OAAO,IAAM,EAAG,SAAS,CAC9B,KAAM,QACN,KAAM,gCACN,QAAS,CAAC,EAAK,KACb,MAAM,EAAS,OAAQ,GAAO,EAAI,QAAW,OAAO,cACpD,GAAe,QAAX,GAA+B,SAAX,EAKT,SAAX,EA7BS,EAAC,EAAK,IAAU,IAAI,QAAQ,CAAC,EAAS,KACrD,MAAM,EAAS,GACf,IAAI,EAAO,EACX,EAAI,GAAG,OAAS,IAEd,GADA,GAAQ,EAAE,OACN,EAAO,EAA6D,OAApD,EAAO,IAAI,MAAM,wBAAoB,EAAI,UAC7D,EAAO,KAAK,KAEd,EAAI,GAAG,MAAO,IAAM,EAAQ,OAAO,OAAO,GAAQ,SAAS,UAC3D,EAAI,GAAG,QAAS,KAqBd,CAAS,EAAK,OAAW,KAAM,IAC7B,MAAM,EAAS,kBAAkB,GACjC,IAAK,EAAO,GAAkD,YAA5C,EAAQ,EAAK,IAAK,CAAE,MAAO,EAAO,QACpD,MAAM,EAAO,EAAO,IACd,EAAW,EAAO,MAGlB,GAA2C,IAA/B,EAAK,sBACvB,IAAK,EAAS,kBAAoB,EAAW,CAC3C,MAAM,EAAM,0BAA4B,wBACxC,EAAS,iBAAmB,EAAY,GAAK,EAAI,gBACnD,CACA,MAAM,EAAU,wBAAwB,EAAU,CAAC,GAC9C,EAAQ,GACb,EAAQ,EAAK,IAAK,CAAE,IAAI,EAAM,SAAU,aAAa,CAAC,KADnC,EAAQ,EAAK,IAAK,CAAE,MAAO,EAAQ,UAErD,MAAO,GAAM,EAAQ,EAAK,IAAK,CAAE,MAAO,OAAO,GAAK,EAAE,SAAW,MAhB3C,EAAQ,EAAK,IAAK,CAAE,MAAO,4BAJlD,IAAM,EAAQ,EAAK,IAAK,aAAa,CAAC,GAAI,CAC1C,MAAO,GAAK,EAAQ,EAAK,IAAK,CAAE,MAAO,OAAO,GAAK,EAAE,SAAW,IAAM,KAqBxE,wCAGV","file":"/npm/codebuddy-first-bridge@1.6.2/home-plugin/codebuddy-indicator/lib/index.mjs","sourceRoot":"","sourcesContent":["/**\n * Minified by jsDelivr using Terser v5.48.0.\n * Original file: /npm/codebuddy-first-bridge@1.6.2/home-plugin/codebuddy-indicator/lib/index.mjs\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n","// codebuddy-indicator — host half.\n//\n// 收集各会话 codebuddy-first-bridge 插件（preset 或动态形态）推送的 codebuddy 运行快照，\n// 维护一张按项目（工作目录 cwd）索引的全局表，并通过 webServer 注册\n// GET /codebuddy-indicator/status 路由暴露给浏览器。\n//\n// 两个数据入口：\n//   1. preset 形态（真 Node 模块，codebuddy-first-bridge preset）：apply 时\n//      ctx.emit('codebuddy/mode', { active: true }) 宣告 codebuddy 优先模式，此后每\n//      30s 心跳续期；每次状态变化 ctx.emit('codebuddy/status', { snapshot }) 推送快照。\n//   2. 动态形态（当前会话的 cordis 动态插件，沙箱无 ctx.emit）：通过\n//      ctx.provide 暴露的 codebuddyCollector 服务调用 mergeSnapshot(snapshot) 推入。\n//\n// presetActive 是「心跳租约」而非粘滞标志（v1.1.1 修复）：preset 每 30s 宣告一次\n// active:true，TTL 75s（≈两拍容差，容忍事件循环抖动）；最后一个 codebuddy-first\n// 会话关闭后心跳停止，租约到期自动熄灭——否则任何会话加载过一次 preset 之后，\n// 所有会话（含非 codebuddy-first 模式）的标题栏都会常驻「CB 就绪」灯。\n//\n// 按会话判定（v1.1.3）：端点返回 presetSessions —— 当前组合了 codebuddy-first\n// preset 的会话 id 名单。客户端只需判断「我的 sessionId 在不在名单里」，在/不在\n// 都是确定答案，不必去猜 DSH 客户端会话摘要里 agentPreset 字段的位置（0.3.14 刚\n// 把它移进 projectionValues，v1.1.2 的判定就此失效；默认会话更是根本没有该字段\n// → 判定「未知」→ 回退全局租约 → 普通会话仍亮灯）。\n//\n// 名单有两个来源，取并集：\n//   A. 实时枚举（首选，权威）：agents.list() 遍历活着的 agent，用\n//      agentPresets.composedPreset(agent.ctx) 读它实际组合的 preset id。\n//      每次请求现算，对已经开着的会话立即生效，不依赖任何上报。\n//   B. preset 上报（兜底）：preset 在 codebuddy/mode 事件里带上自己的 sessionId\n//      并按会话记租约。A 不可用时（服务缺失/DSH 内部 API 变动）仍能工作。\n// 两源都失灵时退回全局租约 presetActive（老语义）。\n//\n// 家级（cordis.patch.yml）插件运行在 host 组合的 root realm；会话内插件的\n// ctx.emit 事件是 app 级广播，不受 isolate realm（只隔离服务）影响。\n//\n// 路由返回 JSON：{ state, running, projects[], presetActive, presetSessions[], lastModeAt }。\n\n// v1.2.0：preset 声明式注册（Form-A 修复）。新 DSH（≥0.1.7-alpha.1）preset 走\n// 声明制、目录式 preset 不再被读取；由 indicator 向 agentPresets 注册表登记\n// codebuddy-first preset（官方 standard 全量 + 桥接行）。机制详见 ./preset-definition.mjs。\nimport { declareCodebuddyFirstPreset } from './preset-definition.mjs'\n// v1.3.2：可视化配置界面 —— 设置文件的读写原语全部来自 core（单一事实源；\n// preset/dynamic/MCP 三形态读同一实现）。相对路径 ../../../core 在部署形态\n// （<profile>/node_modules/codebuddy-first-bridge/home-plugin/codebuddy-indicator/lib/）\n// 与仓库形态下同构成立。\nimport {\n  BACKENDS, SETTINGS_KEYS, readBridgeSettingsFile, writeBridgeSettingsFile, sanitizeBridgeSettings,\n  defaultBridgeSettings, allBackendSettingsMeta, diagnoseBackend, SETTINGS_FILE_NAME\n} from '../../../core/codebuddy-core.mjs'\n\nexport const PRESET_TTL_MS = 75000\nexport const PRESET_ID = 'codebuddy-first'\n\n// MCP 子进程快照文件（v1.1.5）。标准（非 codebuddy-first）模式下 codebuddy 经全局\n// MCP 行调用，那是**独立子进程**：没有 ctx.emit，也拿不到 codebuddyCollector 服务，\n// 所以此前家级插件收不到任何 MCP 路径数据 —— 状态灯在整个调用过程中完全不出现。\n// 现在 MCP 把快照原子写进 dsh-home/<MCP_BRIDGE_FILE>，这里在响应请求时读取合并。\n// 不走 HTTP 反推：webServer.register 不暴露端口，且端口实测会变。\nexport const MCP_BRIDGE_FILE = 'codebuddy-indicator-mcp.json'\nexport const MCP_BRIDGE_STALE_MS = 90000\n\n// 同时在线的 codebuddy-first 会话上限（防御异常上报导致的无界增长）。\nconst MAX_SESSIONS = 64\n\n// 过期快照里仍在 running 的项目降级为已结束：子进程可能被强杀而来不及写收尾\n// 快照，否则灯会永久转圈。纯函数，便于独立测试。\nexport function normalizeMcpBridge(payload, nowMs, staleMs) {\n  if (!payload || typeof payload !== 'object') return null\n  const projects = Array.isArray(payload.projects) ? payload.projects : []\n  const updatedAt = Number(payload.updatedAt) || 0\n  const stale = (Number(nowMs) || 0) - updatedAt > (Number(staleMs) || MCP_BRIDGE_STALE_MS)\n  const out = []\n  for (const p of projects) {\n    if (!p || !p.cwd) continue\n    if (stale && (Number(p.running) || 0) > 0) {\n      out.push(Object.assign({}, p, { running: 0, current: null, state: p.state === 'running' ? 'idle' : p.state }))\n    } else out.push(p)\n  }\n  return { source: 'mcp', updatedAt: updatedAt, stale: stale, projects: out }\n}\n\n// 可独立于 Cordis 测试的纯状态机：注入 now（时钟）、ttlMs（租约时长）与\n// listPresetSessions（实时枚举器，返回 codebuddy-first 会话 id 数组）即可。\nexport function createIndicatorState(opts) {\n  const o = opts || {}\n  const now = typeof o.now === 'function' ? o.now : Date.now\n  const ttlMs = o.ttlMs != null ? o.ttlMs : PRESET_TTL_MS\n  const listPresetSessions = typeof o.listPresetSessions === 'function' ? o.listPresetSessions : null\n  const projects = Object.create(null)\n  const MAX_PROJECTS = 24\n  let presetActiveUntil = 0\n  let lastModeAt = 0\n  // sessionId -> 租约到期时间戳（按会话判定的权威来源）。\n  const sessionLeases = Object.create(null)\n\n  function projectName(cwd) {\n    const s = String(cwd || '')\n    const parts = s.split(/[\\\\/]/).filter(Boolean)\n    return parts.length ? parts[parts.length - 1] : s\n  }\n\n  // codebuddy/mode 事件：active:true 续租；active:false 立即熄灭（preset 当前\n  // 不主动发 false——依赖租约到期——但收到即尊重，面向未来语义完整）。\n  function onMode(payload) {\n    try {\n      const sid = payload && typeof payload.sessionId === 'string' && payload.sessionId ? payload.sessionId : null\n      if (payload && payload.active) {\n        presetActiveUntil = now() + ttlMs\n        lastModeAt = now()\n        if (sid) {\n          sessionLeases[sid] = now() + ttlMs\n          // 上限保护：超出时丢掉最早到期的租约（正常情况过期项已被 snapshot 清掉）。\n          const ids = Object.keys(sessionLeases)\n          if (ids.length > MAX_SESSIONS) {\n            ids.sort((a, b) => sessionLeases[a] - sessionLeases[b])\n            for (let i = 0; i < ids.length - MAX_SESSIONS; i++) delete sessionLeases[ids[i]]\n          }\n        }\n      } else {\n        // 明确下线：带 sessionId 只熄灭该会话（最后一个走时顺带清全局租约，让仍\n        // 读 presetActive 的旧客户端也立刻熄灭）；不带 sessionId 的旧版 preset\n        // 按老语义视为全局下线。\n        if (sid) {\n          delete sessionLeases[sid]\n          if (Object.keys(sessionLeases).length === 0) presetActiveUntil = 0\n        } else {\n          presetActiveUntil = 0\n        }\n      }\n    } catch (e) { }\n  }\n\n  function mergeSnapshot(snap) {\n    if (!snap || typeof snap !== 'object') return\n    const list = Array.isArray(snap.projects) ? snap.projects : (snap.cwd ? [snap] : [])\n    for (const p of list) {\n      if (!p || !p.cwd) continue\n      projects[p.cwd] = {\n        cwd: p.cwd,\n        name: p.name || projectName(p.cwd),\n        state: p.state || 'idle',\n        running: Number(p.running) || 0,\n        current: p.current || null,\n        trail: Array.isArray(p.trail) ? p.trail.slice(-12) : [],\n        lastStatus: p.lastStatus || null,\n        lastAt: p.lastAt || 0,\n        lastSessionId: p.lastSessionId || null,\n        fallbackActive: !!p.fallbackActive,\n        updatedAt: Number(p.updatedAt) || Date.now()\n      }\n    }\n    const keys = Object.keys(projects)\n    if (keys.length > MAX_PROJECTS) {\n      const stale = keys\n        .map((k) => projects[k])\n        .filter((p) => p.running === 0 && !p.fallbackActive)\n        .sort((a, b) => a.updatedAt - b.updatedAt)\n      const drop = Math.max(0, keys.length - MAX_PROJECTS)\n      for (let i = 0; i < drop && i < stale.length; i++) delete projects[stale[i].cwd]\n    }\n  }\n\n  // 计算对外快照（纯函数式：不修改内部表）。\n  function snapshot() {\n    const t = now()\n    // 会话名单 = 实时枚举（权威）∪ preset 上报租约（兜底）。\n    const seen = Object.create(null)\n    const presetSessions = []\n    if (listPresetSessions) {\n      try {\n        const live = listPresetSessions()\n        if (Array.isArray(live)) {\n          for (const sid of live) {\n            if (typeof sid === 'string' && sid && !seen[sid]) { seen[sid] = 1; presetSessions.push(sid) }\n          }\n        }\n      } catch (e) { }\n    }\n    // 顺手清理过期上报租约。\n    for (const sid of Object.keys(sessionLeases)) {\n      if (t < sessionLeases[sid]) {\n        if (!seen[sid]) { seen[sid] = 1; presetSessions.push(sid) }\n      } else delete sessionLeases[sid]\n    }\n    const presetActive = t < presetActiveUntil || presetSessions.length > 0\n    const OK_HOLD_MS = presetActive ? 10 * 60 * 1000 : 8000\n    const STALE_MS = 10 * 60 * 1000\n    const list = Object.keys(projects)\n      .map((k) => projects[k])\n      .filter((p) => {\n        const age = t - (Number(p.updatedAt) || 0)\n        if (p.running > 0 || p.fallbackActive) return true\n        if (p.state === 'running') return true\n        if (p.state === 'ok' || p.state === 'failed') return age < OK_HOLD_MS\n        return age < STALE_MS\n      })\n      .sort((a, b) => b.updatedAt - a.updatedAt)\n    const running = list.reduce((n, p) => n + p.running, 0)\n    const state = running > 0 ? 'running' : (list.some((p) => p.fallbackActive) ? 'fallback' : (list.length ? 'ok' : 'idle'))\n    return { state, running, projects: list, presetActive, presetSessions, lastModeAt }\n  }\n\n  return { onMode, mergeSnapshot, snapshot }\n}\n\nexport const name = 'codebuddy-indicator'\nexport const inject = []\n\n// v1.3.2 可视化配置界面：设置文件 GET/POST 的纯函数面（可脱离 DSH 单测）。\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** token 掩码：只回传是否已设置 + 尾 4 位，绝不把明文 token 送回浏览器。 */\nexport function maskToken(tok) {\n  const s = String(tok || '')\n  if (!s) return { set: false, hint: '' }\n  return { set: true, hint: s.length <= 8 ? '••••' : '••••' + s.slice(-4) }\n}\n\n/**\n * 设置面板的完整视图（GET 响应体）：当前值 + 元数据 + 逐后端诊断 + 掩码 token。\n * @param {{readFileSync?:Function,writeFileSync?:Function,path?:string|null,env?:object,dir?:string}} [io]\n */\nexport function settingsView(io) {\n  const o = io || {}\n  const fromFile = readBridgeSettingsFile(o)\n  const snap = fromFile || defaultBridgeSettings()\n  return {\n    fileName: SETTINGS_FILE_NAME,\n    persisted: fromFile !== null,\n    preferredBackend: snap.preferredBackend,\n    defaultModel: snap.defaultModel,\n    endpointOverride: snap.endpointOverride,\n    // token 明文永不外传：只给「是否已设置 + 尾 4 位掩码」。POST 时留空 = 保持现值。\n    codebuddyEnToken: maskToken(snap.codebuddyEnToken),\n    backends: allBackendSettingsMeta(o),\n    diagnostics: BACKENDS.map((b) => diagnoseBackend(b, snap, o))\n  }\n}\n\n/** POST body（文本）→ {ok, raw?, value?|error?}：只认四个已知字段，全清洗。 */\nexport function parseSettingsBody(text) {\n  if (typeof text !== 'string' || !text.trim()) return { ok: false, error: 'empty body' }\n  let raw\n  try { raw = JSON.parse(text) } catch (e) { return { ok: false, error: 'invalid JSON' } }\n  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, error: 'body must be an object' }\n  for (const k of SETTINGS_KEYS) {\n    const v = raw[k]\n    if (v !== undefined && typeof v !== 'string') return { ok: false, error: 'field ' + k + ' must be a string' }\n  }\n  return { ok: true, raw: raw, value: sanitizeBridgeSettings(raw) }\n}\n\nexport function apply(ctx) {\n  // 实时枚举器：遍历活着的 agent，读它实际组合的 preset id。全程防御——任一\n  // 服务或字段缺失（DSH 内部 API 变动）都只是返回空数组，退回 preset 上报兜底。\n  function listPresetSessions() {\n    try {\n      const agents = typeof ctx.get === 'function' ? ctx.get('agents') : undefined\n      const presets = typeof ctx.get === 'function' ? ctx.get('agentPresets') : undefined\n      if (!agents || typeof agents.list !== 'function') return []\n      if (!presets || typeof presets.composedPreset !== 'function') return []\n      const out = []\n      for (const agent of agents.list()) {\n        try {\n          if (!agent || !agent.ctx) continue\n          if (presets.composedPreset(agent.ctx) !== PRESET_ID) continue\n          const sid = agent.id || (agent.session && agent.session.id)\n          if (typeof sid === 'string' && sid) out.push(sid)\n        } catch (e) { }\n      }\n      return out\n    } catch (e) { return [] }\n  }\n\n  const st = createIndicatorState({ listPresetSessions })\n\n  ctx.on('codebuddy/mode', st.onMode)\n\n  ctx.on('codebuddy/status', (payload) => {\n    try {\n      const snap = payload && typeof payload === 'object' && payload.snapshot ? payload.snapshot : payload\n      st.mergeSnapshot(snap)\n    } catch (e) { }\n  })\n\n  try {\n    ctx.provide('codebuddyCollector', { mergeSnapshot: st.mergeSnapshot })\n  } catch (e) { }\n\n  // MCP 子进程快照文件的读取端。dsh-home 从本模块位置推导：\n  // <dsh-home>/plugins/codebuddy-indicator/lib/index.mjs → 上溯三级目录。\n  // 读取在响应请求时进行（客户端本就在轮询，天然限频），失败一律忽略。\n  function builtin(name) {\n    try {\n      return (globalThis.process && typeof globalThis.process.getBuiltinModule === 'function')\n        ? globalThis.process.getBuiltinModule(name)\n        : null\n    } catch (e) { return null }\n  }\n  const nodeFs = builtin('node:fs')\n  const nodePath = builtin('node:path')\n  const nodeUrl = builtin('node:url')\n\n  let bridgePath = null\n  try {\n    const envDir = globalThis.process && globalThis.process.env\n      ? (globalThis.process.env.CODEBUDDY_INDICATOR_DIR || globalThis.process.env.DSH_HOME)\n      : null\n    if (envDir && nodePath) {\n      bridgePath = nodePath.join(envDir, MCP_BRIDGE_FILE)\n    } else if (nodePath && nodeUrl && import.meta.url) {\n      const libDir = nodePath.dirname(nodeUrl.fileURLToPath(import.meta.url))     // .../lib\n      const home = nodePath.resolve(libDir, '..', '..', '..')                     // dsh-home\n      bridgePath = nodePath.join(home, MCP_BRIDGE_FILE)\n    }\n  } catch (e) { bridgePath = null }\n\n  let lastBridgeStamp = 0\n  function ingestMcpBridge() {\n    if (!bridgePath || !nodeFs) return\n    try {\n      const stamp = Number(nodeFs.statSync(bridgePath).mtimeMs) || 0\n      // 未变更就不重复合并：mergeSnapshot 会把缺失的 updatedAt 补成 now，重复\n      // 合并会让过期数据一直显得很新，从而绕过快照的过期判定。\n      if (stamp === lastBridgeStamp) return\n      const norm = normalizeMcpBridge(JSON.parse(nodeFs.readFileSync(bridgePath, 'utf8')), Date.now(), MCP_BRIDGE_STALE_MS)\n      if (norm && norm.projects.length) st.mergeSnapshot(norm)\n      lastBridgeStamp = stamp\n    } catch (e) { }\n  }\n\n  // v1.2.0：Form-A 修复 —— 新 DSH（≥0.1.7-alpha.1）preset 走声明制，目录式\n  // preset 不再被读取；indicator 以自身为载体向 agentPresets 注册表登记\n  // codebuddy-first preset（官方 standard 全量 + 桥接行）。旧 DSH 上\n  // agentPresets 永不出现，注入器挂起无副作用（loader 不收集等待中的注入器）。\n  // 详见 ./preset-definition.mjs 头注。\n  try {\n    declareCodebuddyFirstPreset(ctx)\n  } catch (e) { /* 注册通道任何异常都不能影响状态灯本业 */ }\n\n  if (typeof ctx.inject === 'function') {\n    ctx.inject(['webServer'], (webCtx) => {\n      const ws = webCtx.get('webServer')\n      if (!ws || typeof ws.register !== 'function') return\n      webCtx.effect(() => ws.register({\n        kind: 'exact',\n        path: '/codebuddy-indicator/status',\n        handler: (req, res) => {\n          try {\n            ingestMcpBridge()\n            const body = JSON.stringify(st.snapshot())\n            res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })\n            res.end(body)\n          } catch (e) {\n            res.writeHead(500, { 'Content-Type': 'application/json' })\n            res.end(JSON.stringify({ error: String(e && e.message || e) }))\n          }\n        }\n      }), 'codebuddy-indicator: status route')\n\n      // ── v1.3.2：可视化配置界面的读写路由 ──────────────────────────────────\n      // webServer.register 不做方法路由 → handler 内自己分发。单一写者（本 host\n      // 进程），文件原子写；preset/dynamic/MCP 三形态在每次调用时现读该文件，\n      // 保存即生效、无需重启。GET 永不回传 token 明文（见 settingsView/maskToken）。\n\n      const readBody = (req, limit) => new Promise((resolve, reject) => {\n        const chunks = []\n        let size = 0\n        req.on('data', (c) => {\n          size += c.length\n          if (size > limit) { reject(new Error('body too large')); req.destroy(); return }\n          chunks.push(c)\n        })\n        req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))\n        req.on('error', reject)\n      })\n\n      const jsonRes = (res, code, obj) => {\n        try {\n          res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })\n          res.end(JSON.stringify(obj))\n        } catch (e) { /* 连接可能已被对端关闭 */ }\n      }\n\n      webCtx.effect(() => ws.register({\n        kind: 'exact',\n        path: '/codebuddy-indicator/settings',\n        handler: (req, res) => {\n          const method = String((req && req.method) || 'GET').toUpperCase()\n          if (method === 'GET' || method === 'HEAD') {\n            try { jsonRes(res, 200, settingsView({})) }\n            catch (e) { jsonRes(res, 500, { error: String(e && e.message || e) }) }\n            return\n          }\n          if (method !== 'POST') { jsonRes(res, 405, { error: 'method not allowed' }); return }\n          readBody(req, 64 * 1024).then((text) => {\n            const parsed = parseSettingsBody(text)\n            if (!parsed.ok) { jsonRes(res, 400, { error: parsed.error }); return }\n            const body = parsed.raw\n            const incoming = parsed.value\n            // token 语义：字段缺省或空串 = 保持现值（GET 不回传明文，面板无法回填）；\n            // 显式清除用 codebuddyEnTokenClear:true。\n            const wantClear = body.codebuddyEnTokenClear === true\n            if (!incoming.codebuddyEnToken || wantClear) {\n              const cur = readBridgeSettingsFile() || defaultBridgeSettings()\n              incoming.codebuddyEnToken = wantClear ? '' : cur.codebuddyEnToken\n            }\n            const written = writeBridgeSettingsFile(incoming, {})\n            if (!written.ok) { jsonRes(res, 500, { error: written.error }); return }\n            jsonRes(res, 200, { ok: true, settings: settingsView({}) })\n          }).catch((e) => jsonRes(res, 400, { error: String(e && e.message || e) }))\n        }\n      }), 'codebuddy-indicator: settings route')\n    })\n  }\n}\n"]}