@x-otto/cli 0.0.1-alpha.43 → 0.0.1-alpha.44

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 (60) hide show
  1. package/dist/build-id-D_JURZdd.js +3 -0
  2. package/dist/build-id-D_JURZdd.js.map +1 -0
  3. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -1
  4. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +2 -1
  5. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +5 -1
  6. package/dist/{command-dispatcher-DhcLUPR5.js → command-dispatcher-Dxj41r5P.js} +4 -4
  7. package/dist/{command-dispatcher-DhcLUPR5.js.map → command-dispatcher-Dxj41r5P.js.map} +1 -1
  8. package/dist/{daemon-base-Bc9p51Um.js → daemon-base-Y1XI61JY.js} +1 -1
  9. package/dist/{daemon-base-Bc9p51Um.js.map → daemon-base-Y1XI61JY.js.map} +1 -1
  10. package/dist/{doctor-lT2QhG2q.js → doctor-DXBLkbkP.js} +9 -9
  11. package/dist/doctor-DXBLkbkP.js.map +1 -0
  12. package/dist/{doctor-attach-_zeIJewx.js → doctor-attach-DEMuyBOk.js} +2 -2
  13. package/dist/{doctor-attach-_zeIJewx.js.map → doctor-attach-DEMuyBOk.js.map} +1 -1
  14. package/dist/{doctor-cdp-K_mIFRfA.js → doctor-cdp-CnJUVNOB.js} +1 -1
  15. package/dist/{doctor-cdp-K_mIFRfA.js.map → doctor-cdp-CnJUVNOB.js.map} +1 -1
  16. package/dist/{doctor-heap-Rr2nNvcw.js → doctor-heap-Eu3LZj-F.js} +2 -2
  17. package/dist/{doctor-heap-Rr2nNvcw.js.map → doctor-heap-Eu3LZj-F.js.map} +1 -1
  18. package/dist/{doctor-ps-OL3FGQYu.js → doctor-ps-JBgCH77n.js} +2 -2
  19. package/dist/{doctor-ps-OL3FGQYu.js.map → doctor-ps-JBgCH77n.js.map} +1 -1
  20. package/dist/export-trajectory-D-BcH0Wt.js +14 -0
  21. package/dist/export-trajectory-D-BcH0Wt.js.map +1 -0
  22. package/dist/{feedback-BQ_oz5nh.js → feedback-CYFY2MC4.js} +2 -2
  23. package/dist/{feedback-BQ_oz5nh.js.map → feedback-CYFY2MC4.js.map} +1 -1
  24. package/dist/feedback-D7Po4mlE.js +2 -0
  25. package/dist/{feishu-setup-B6ETf-8Y.js → feishu-setup-5ZoxtH0J.js} +1 -1
  26. package/dist/{feishu-setup-B6ETf-8Y.js.map → feishu-setup-5ZoxtH0J.js.map} +1 -1
  27. package/dist/{ftue-doctor-recovery-DeAf8jVo.js → ftue-doctor-recovery-8FeDBFOc.js} +1 -1
  28. package/dist/{ftue-doctor-recovery-DeAf8jVo.js.map → ftue-doctor-recovery-8FeDBFOc.js.map} +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/{mcp-9P_AmnDk.js → mcp-Cm4-OhXI.js} +1 -1
  33. package/dist/{mcp-9P_AmnDk.js.map → mcp-Cm4-OhXI.js.map} +1 -1
  34. package/dist/{mcp-server-BK4L-YBN.js → mcp-server-Dpza3yn2.js} +1 -1
  35. package/dist/{mcp-server-BK4L-YBN.js.map → mcp-server-Dpza3yn2.js.map} +1 -1
  36. package/dist/{migrate-DDNkik_0.js → migrate-RlBJqJnB.js} +1 -1
  37. package/dist/{migrate-DDNkik_0.js.map → migrate-RlBJqJnB.js.map} +1 -1
  38. package/dist/{observe-CzIv09hO.js → observe-Dxfud-t7.js} +1 -1
  39. package/dist/{observe-CzIv09hO.js.map → observe-Dxfud-t7.js.map} +1 -1
  40. package/dist/parse-cli-BSJEQtBo.js +3 -0
  41. package/dist/parse-cli-BSJEQtBo.js.map +1 -0
  42. package/dist/{persistence-DOaizHMA.js → persistence-CRFhOYx6.js} +2 -2
  43. package/dist/{persistence-DOaizHMA.js.map → persistence-CRFhOYx6.js.map} +1 -1
  44. package/dist/{plugin-assembly-check-Ov17ra7G.js → plugin-assembly-check-DEozmcJ7.js} +1 -1
  45. package/dist/{plugin-assembly-check-Ov17ra7G.js.map → plugin-assembly-check-DEozmcJ7.js.map} +1 -1
  46. package/dist/{ps-BovE9XMQ.js → ps-DiKCLKbs.js} +1 -1
  47. package/dist/{ps-BovE9XMQ.js.map → ps-DiKCLKbs.js.map} +1 -1
  48. package/dist/{run-attach-z7YECztf.js → run-attach-DDpMk2_N.js} +1 -1
  49. package/dist/{run-attach-z7YECztf.js.map → run-attach-DDpMk2_N.js.map} +1 -1
  50. package/dist/{serve-DaxogAZ5.js → serve-Ke0nwN9Y.js} +2 -2
  51. package/dist/{serve-DaxogAZ5.js.map → serve-Ke0nwN9Y.js.map} +1 -1
  52. package/dist/{version-Eh8zZKgk.js → version-BKPdYoHG.js} +2 -2
  53. package/dist/{version-Eh8zZKgk.js.map → version-BKPdYoHG.js.map} +1 -1
  54. package/package.json +12 -12
  55. package/dist/build-id-ClyMIKLy.js +0 -3
  56. package/dist/build-id-ClyMIKLy.js.map +0 -1
  57. package/dist/doctor-lT2QhG2q.js.map +0 -1
  58. package/dist/feedback-C2Hdxp1y.js +0 -2
  59. package/dist/parse-cli-3fWEIMWc.js +0 -3
  60. package/dist/parse-cli-3fWEIMWc.js.map +0 -1
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  function e(e){return new Promise(t=>{if(e){if(e.aborted){t();return}e.addEventListener(`abort`,()=>t(),{once:!0});return}let n=()=>{process.off(`SIGINT`,n),process.off(`SIGTERM`,n),t()};process.on(`SIGINT`,n),process.on(`SIGTERM`,n)})}export{e as t};
3
- //# sourceMappingURL=daemon-base-Bc9p51Um.js.map
3
+ //# sourceMappingURL=daemon-base-Y1XI61JY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-base-Bc9p51Um.js","names":[],"sources":["../src/commands/daemon-base.ts"],"sourcesContent":["/**\n * daemon-base.ts — 常驻进程启动器共享基座。\n *\n * `serve`/`persistenced`/`remote-persistence-server` 三个子命令各自启动一个\n * 常驻服务进程,生命周期形状相同(启动 → 打印监听地址 → 等待信号 → 优雅关闭),\n * 唯一差异是启动的具体服务。`waitForShutdown` 此前在三个文件里逐字重复(终局架构\n * review 发现),此处收敛为单一真源。\n */\n\n/**\n * 等待进程收到关闭信号(外部 AbortSignal,或退化到监听 SIGINT/SIGTERM)。\n *\n * `signal` 由测试注入(模拟主动关闭);生产路径缺省走 SIGINT/SIGTERM,\n * 监听器在触发后自行摘除,不留悬挂 listener。\n */\nexport function waitForShutdown(signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal) {\n if (signal.aborted) {\n resolve()\n return\n }\n signal.addEventListener('abort', () => resolve(), { once: true })\n return\n }\n const onSignal = () => {\n process.off('SIGINT', onSignal)\n process.off('SIGTERM', onSignal)\n resolve()\n }\n process.on('SIGINT', onSignal)\n process.on('SIGTERM', onSignal)\n })\n}\n"],"mappings":";AAeA,SAAgB,EAAgB,EAAqC,CACnE,OAAO,IAAI,QAAS,GAAY,CAC9B,GAAI,EAAQ,CACV,GAAI,EAAO,QAAS,CAClB,GAAS,CACT,OAEF,EAAO,iBAAiB,YAAe,GAAS,CAAE,CAAE,KAAM,GAAM,CAAC,CACjE,OAEF,IAAM,MAAiB,CACrB,QAAQ,IAAI,SAAU,EAAS,CAC/B,QAAQ,IAAI,UAAW,EAAS,CAChC,GAAS,EAEX,QAAQ,GAAG,SAAU,EAAS,CAC9B,QAAQ,GAAG,UAAW,EAAS,EAC/B"}
1
+ {"version":3,"file":"daemon-base-Y1XI61JY.js","names":[],"sources":["../src/commands/daemon-base.ts"],"sourcesContent":["/**\n * daemon-base.ts — 常驻进程启动器共享基座。\n *\n * `serve`/`persistenced`/`remote-persistence-server` 三个子命令各自启动一个\n * 常驻服务进程,生命周期形状相同(启动 → 打印监听地址 → 等待信号 → 优雅关闭),\n * 唯一差异是启动的具体服务。`waitForShutdown` 此前在三个文件里逐字重复(终局架构\n * review 发现),此处收敛为单一真源。\n */\n\n/**\n * 等待进程收到关闭信号(外部 AbortSignal,或退化到监听 SIGINT/SIGTERM)。\n *\n * `signal` 由测试注入(模拟主动关闭);生产路径缺省走 SIGINT/SIGTERM,\n * 监听器在触发后自行摘除,不留悬挂 listener。\n */\nexport function waitForShutdown(signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal) {\n if (signal.aborted) {\n resolve()\n return\n }\n signal.addEventListener('abort', () => resolve(), { once: true })\n return\n }\n const onSignal = () => {\n process.off('SIGINT', onSignal)\n process.off('SIGTERM', onSignal)\n resolve()\n }\n process.on('SIGINT', onSignal)\n process.on('SIGTERM', onSignal)\n })\n}\n"],"mappings":";AAeA,SAAgB,EAAgB,EAAqC,CACnE,OAAO,IAAI,QAAS,GAAY,CAC9B,GAAI,EAAQ,CACV,GAAI,EAAO,QAAS,CAClB,GAAS,CACT,OAEF,EAAO,iBAAiB,YAAe,GAAS,CAAE,CAAE,KAAM,GAAM,CAAC,CACjE,OAEF,IAAM,MAAiB,CACrB,QAAQ,IAAI,SAAU,EAAS,CAC/B,QAAQ,IAAI,UAAW,EAAS,CAChC,GAAS,EAEX,QAAQ,GAAG,SAAU,EAAS,CAC9B,QAAQ,GAAG,UAAW,EAAS,EAC/B"}
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import{arch as e,cpus as t,freemem as n,homedir as r,platform as i,release as a,totalmem as o}from"node:os";import{join as s}from"node:path";import{accessSync as c,existsSync as l,statSync as u}from"node:fs";import{SESSION_MAX_HISTORY_MESSAGES as d,SESSION_MAX_PER_SESSION_BYTES as f,SESSION_MIN_PER_SESSION_BYTES as p,SESSION_RESUME_WINDOW_MESSAGES as m,SESSION_TOTAL_RESIDENCY_BUDGET as h}from"@x-otto/session-contract";const g=s(r(),`.otto`);async function _(e,t){let n=t.stdout,r=t.stderr,i=e.find(e=>!e.startsWith(`-`));if(i===`ps`||i===`attach`||i===`heap`){let n=e.filter(e=>e!==i);switch(i){case`ps`:{let{runDoctorPs:e}=await import(`./doctor-ps-OL3FGQYu.js`);return e(n,t)}case`attach`:{let{runDoctorAttach:e}=await import(`./doctor-attach-_zeIJewx.js`);return e(n,t)}case`heap`:{let{runDoctorHeap:e}=await import(`./doctor-heap-Rr2nNvcw.js`);return e(n,t)}}}if(i!==void 0)return r(`otto doctor: unknown subcommand "${i}" (available: ps, attach, heap)\n`),1;if(e.includes(`--help`)||e.includes(`-h`))return k(n),0;let a=e.includes(`--json`)?`json`:e.includes(`--summary`)?`summary`:`detailed`;try{let e=await v();return a===`json`?n(JSON.stringify(e,null,2)+`
3
- `):a===`summary`?D(n,e):O(n,e),0}catch(e){return r(`otto doctor: ${e instanceof Error?e.message:String(e)}\n`),1}}async function v(){let e=await y(),t=await b(),n=await C(),r=await w(),{plugins:i,providerPluginDiscovered:a}=await x();return{environment:e,configuration:t,state:n,connectivity:r,plugins:i,ftue:await S(a)}}async function y(){return{runtime:`Node.js ${process.version}`,installPath:process.execPath,shell:process.env.SHELL??`(unknown)`,terminal:process.env.TERM??`(unknown)`,cpu:`${t().length}×${e()}`,memory:{total:o(),free:n()},platform:i(),release:a()}}async function b(){let e=process.env.ANTHROPIC_API_KEY?`api-key (ANTHROPIC_API_KEY)`:process.env.OTTO_API_KEY?`api-key (OTTO_API_KEY)`:process.env.AWS_ACCESS_KEY_ID?`aws-bedrock (env keys)`:`not configured`;return{workspaceDir:process.cwd(),storageDir:g,hasRipgrep:await T(`rg`),hasGit:await T(`git`),authMode:e,residencyBudget:{totalBytes:h,minPerSessionBytes:p,maxPerSessionBytes:f,historyMessages:d,resumeWindowMessages:m}}}async function x(){try{let{discoverPluginsWithBundled:e}=await import(`./discover-with-bundled-B5d4q___.js`),{hostSelfDescribingOptions:t}=await import(`./create-host-app-DG58-vRA.js`),{checkPluginAssembly:n,formatAssemblyGap:i}=await import(`./plugin-assembly-check-Ov17ra7G.js`),a=e({cwd:process.cwd(),homedir:r()}),o=t().pluginBundledDirs??[],s=a.some(e=>e.manifest.capabilities?.includes(`provider`)),c=a.filter(e=>e.scope===`bundled`).length>0&&o.length===0,l=n({discovered:a.map(e=>({id:e.id,scope:e.scope})),active:c?a.filter(e=>e.scope!==`bundled`).map(e=>({id:e.id,scope:e.scope})):a.map(e=>({id:e.id,scope:e.scope}))});return{plugins:{discovered:a.length,gaps:i(l),missing:l.missingFromRuntime},providerPluginDiscovered:s}}catch{return{plugins:void 0,providerPluginDiscovered:!1}}}async function S(e){try{let{createDefaultAuthStore:t}=await import(`@x-otto/ai`),{deriveDoctorFtueGuidance:n}=await import(`./ftue-doctor-recovery-DeAf8jVo.js`),{SettingsManager:r}=await import(`@x-otto/setting`),{OTTO_CONFIG_FILENAME:i}=await import(`@x-otto/env`),a=t({watchForExternalChanges:!1});await a.ensureInitialized();let o=a.listProviders().length>0,c=new r({projectConfigPath:s(g,i)});return await c.load(),n({providerPluginDiscovered:e,hasAnyCredential:o,firstPromptSubmitted:typeof c.get(`first_prompt_submitted_at`)==`number`})}catch{return}}async function C(){let e=s(g,`storage.db`);if(!l(e))return{dbPath:e,dbExists:!1};try{let{SqlitePersistence:t}=await import(`@x-otto/persistence`),n=new t({dbPath:e,namespace:`doctor`,wal:!1});try{let t=n.getInfo();return{dbPath:e,dbExists:!0,dbSize:E(e),...t}}finally{n.close()}}catch{return{dbPath:e,dbExists:!0,integrity:`error (unable to open)`}}}async function w(){let e=`api.anthropic.com`,t=!1,n,r=Date.now();try{let i=new AbortController,a=setTimeout(()=>i.abort(),5e3);await fetch(`https://${e}`,{method:`HEAD`,signal:i.signal}),clearTimeout(a),t=!0,n=Date.now()-r}catch{t=!1}return{providerHost:e,providerReachable:t,providerLatency:n,storageHostConfigured:!1}}async function T(e){try{return c(s(`/usr/local/bin`,e),1),!0}catch{try{return c(s(`/usr/bin`,e),1),!0}catch{return!1}}}function E(e){try{return u(e).size}catch{return 0}}function D(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t,o=i?.dbExists&&i.integrity===`ok`?`✅`:`❌`,s=a.providerReachable?`✅`:`❌`,c=t.plugins&&t.plugins.gaps.length>0?` ⚠ 插件装配缺口 ${t.plugins.missing.length} 项(otto doctor 查看详情)`:``,l=t.ftue&&t.ftue.status!==`looks-configured`?` ⚠ ${t.ftue.summary} → ${t.ftue.recoveryAction}`:``;e([`otto doctor — ${n.runtime} | ${r.storageDir} | DB:${o} ${s}`,i?` ${i.snapshots??`?`} snapshots, ${i.changeLogEntries??`?`} change_log`:``,c,l].filter(Boolean).join(`
2
+ import{arch as e,cpus as t,freemem as n,homedir as r,platform as i,release as a,totalmem as o}from"node:os";import{join as s}from"node:path";import{accessSync as c,constants as l,existsSync as u,statSync as d}from"node:fs";import{SESSION_MAX_HISTORY_MESSAGES as f,SESSION_MAX_PER_SESSION_BYTES as p,SESSION_MIN_PER_SESSION_BYTES as m,SESSION_RESUME_WINDOW_MESSAGES as h,SESSION_TOTAL_RESIDENCY_BUDGET as g}from"@x-otto/session-contract";const _=s(r(),`.otto`);async function v(e,t){let n=t.stdout,r=t.stderr,i=e.find(e=>!e.startsWith(`-`));if(i===`ps`||i===`attach`||i===`heap`){let n=e.filter(e=>e!==i);switch(i){case`ps`:{let{runDoctorPs:e}=await import(`./doctor-ps-JBgCH77n.js`);return e(n,t)}case`attach`:{let{runDoctorAttach:e}=await import(`./doctor-attach-DEMuyBOk.js`);return e(n,t)}case`heap`:{let{runDoctorHeap:e}=await import(`./doctor-heap-Eu3LZj-F.js`);return e(n,t)}}}if(i!==void 0)return r(`otto doctor: unknown subcommand "${i}" (available: ps, attach, heap)\n`),1;if(e.includes(`--help`)||e.includes(`-h`))return A(n),0;let a=e.includes(`--json`)?`json`:e.includes(`--summary`)?`summary`:`detailed`;try{let e=await y();return a===`json`?n(JSON.stringify(e,null,2)+`
3
+ `):a===`summary`?O(n,e):k(n,e),0}catch(e){return r(`otto doctor: ${e instanceof Error?e.message:String(e)}\n`),1}}async function y(){let e=await b(),t=await x(),n=await w(),r=await T(),{plugins:i,providerPluginDiscovered:a}=await S();return{environment:e,configuration:t,state:n,connectivity:r,plugins:i,ftue:await C(a)}}async function b(){return{runtime:`Node.js ${process.version}`,installPath:process.execPath,shell:process.env.SHELL??`(unknown)`,terminal:process.env.TERM??`(unknown)`,cpu:`${t().length}×${e()}`,memory:{total:o(),free:n()},platform:i(),release:a()}}async function x(){let e=process.env.ANTHROPIC_API_KEY?`api-key (ANTHROPIC_API_KEY)`:process.env.OTTO_API_KEY?`api-key (OTTO_API_KEY)`:process.env.AWS_ACCESS_KEY_ID?`aws-bedrock (env keys)`:`not configured`;return{workspaceDir:process.cwd(),storageDir:_,hasRipgrep:await E(`rg`),hasGit:await E(`git`),authMode:e,residencyBudget:{totalBytes:g,minPerSessionBytes:m,maxPerSessionBytes:p,historyMessages:f,resumeWindowMessages:h}}}async function S(){try{let{discoverPluginsWithBundled:e}=await import(`./discover-with-bundled-B5d4q___.js`),{hostSelfDescribingOptions:t}=await import(`./create-host-app-DG58-vRA.js`),{checkPluginAssembly:n,formatAssemblyGap:i}=await import(`./plugin-assembly-check-DEozmcJ7.js`),a=e({cwd:process.cwd(),homedir:r()}),o=t().pluginBundledDirs??[],s=a.some(e=>e.manifest.capabilities?.includes(`provider`)),c=a.filter(e=>e.scope===`bundled`).length>0&&o.length===0,l=n({discovered:a.map(e=>({id:e.id,scope:e.scope})),active:c?a.filter(e=>e.scope!==`bundled`).map(e=>({id:e.id,scope:e.scope})):a.map(e=>({id:e.id,scope:e.scope}))});return{plugins:{discovered:a.length,gaps:i(l),missing:l.missingFromRuntime},providerPluginDiscovered:s}}catch{return{plugins:void 0,providerPluginDiscovered:!1}}}async function C(e){try{let{createDefaultAuthStore:t}=await import(`@x-otto/ai`),{deriveDoctorFtueGuidance:n}=await import(`./ftue-doctor-recovery-8FeDBFOc.js`),{SettingsManager:r}=await import(`@x-otto/setting`),{OTTO_CONFIG_FILENAME:i}=await import(`@x-otto/env`),a=t({watchForExternalChanges:!1});await a.ensureInitialized();let o=a.listProviders().length>0,c=new r({projectConfigPath:s(_,i)});return await c.load(),n({providerPluginDiscovered:e,hasAnyCredential:o,firstPromptSubmitted:typeof c.get(`first_prompt_submitted_at`)==`number`})}catch{return}}async function w(){let e=s(_,`storage.db`);if(!u(e))return{dbPath:e,dbExists:!1};try{let{SqlitePersistence:t}=await import(`@x-otto/persistence`),n=new t({dbPath:e,namespace:`doctor`,wal:!1});try{let t=n.getInfo();return{dbPath:e,dbExists:!0,dbSize:D(e),...t}}finally{n.close()}}catch{return{dbPath:e,dbExists:!0,integrity:`error (unable to open)`}}}async function T(){let e=`api.anthropic.com`,t=!1,n,r=Date.now();try{let i=new AbortController,a=setTimeout(()=>i.abort(),5e3);await fetch(`https://${e}`,{method:`HEAD`,signal:i.signal}),clearTimeout(a),t=!0,n=Date.now()-r}catch{t=!1}return{providerHost:e,providerReachable:t,providerLatency:n,storageHostConfigured:!1}}async function E(e){let t=new Set;for(let e of(process.env.PATH??``).split(`:`))e&&t.add(e);t.add(`/usr/local/bin`),t.add(`/usr/bin`);for(let n of t)try{return c(s(n,e),l.X_OK),!0}catch{}return!1}function D(e){try{return d(e).size}catch{return 0}}function O(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t,o=i?.dbExists&&i.integrity===`ok`?`✅`:`❌`,s=a.providerReachable?`✅`:`❌`,c=t.plugins&&t.plugins.gaps.length>0?` ⚠ 插件装配缺口 ${t.plugins.missing.length} 项(otto doctor 查看详情)`:``,l=t.ftue&&t.ftue.status!==`looks-configured`?` ⚠ ${t.ftue.summary} → ${t.ftue.recoveryAction}`:``;e([`otto doctor — ${n.runtime} | ${r.storageDir} | DB:${o} ${s}`,i?` ${i.snapshots??`?`} snapshots, ${i.changeLogEntries??`?`} change_log`:``,c,l].filter(Boolean).join(`
4
4
  `)+`
5
- `)}function O(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t;if(e(`
5
+ `)}function k(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t;if(e(`
6
6
  Environment
7
- `),e(` Runtime: ${n.runtime}\n`),e(` Install: ${n.installPath}\n`),e(` Shell: ${n.shell}\n`),e(` Terminal: ${n.terminal} (${n.release})\n`),e(` CPU: ${n.cpu}\n`),e(` Memory: ${A(n.memory.free)} / ${A(n.memory.total)} free\n`),r.residencyBudget){let t=r.residencyBudget;e(` Residency: budget=${A(t.totalBytes)} per-session=${A(t.minPerSessionBytes)}~${A(t.maxPerSessionBytes)} history=${t.historyMessages} resume=${t.resumeWindowMessages}\n`)}if(e(`
7
+ `),e(` Runtime: ${n.runtime}\n`),e(` Install: ${n.installPath}\n`),e(` Shell: ${n.shell}\n`),e(` Terminal: ${n.terminal} (${n.release})\n`),e(` CPU: ${n.cpu}\n`),e(` Memory: ${j(n.memory.free)} / ${j(n.memory.total)} free\n`),r.residencyBudget){let t=r.residencyBudget;e(` Residency: budget=${j(t.totalBytes)} per-session=${j(t.minPerSessionBytes)}~${j(t.maxPerSessionBytes)} history=${t.historyMessages} resume=${t.resumeWindowMessages}\n`)}if(e(`
8
8
  Configuration
9
- `),e(` Workspace: ${r.workspaceDir}\n`),e(` Storage: ${r.storageDir} ${l(r.storageDir)?`✅`:`❌`}\n`),e(` ripgrep: ${r.hasRipgrep?`✅`:`❌`}\n`),e(` git: ${r.hasGit?`✅`:`❌`}\n`),e(` Auth: ${r.authMode}\n`),t.plugins)if(e(`
9
+ `),e(` Workspace: ${r.workspaceDir}\n`),e(` Storage: ${r.storageDir} ${u(r.storageDir)?`✅`:`❌`}\n`),e(` ripgrep: ${r.hasRipgrep?`✅`:`❌`}\n`),e(` git: ${r.hasGit?`✅`:`❌`}\n`),e(` Auth: ${r.authMode}\n`),t.plugins)if(e(`
10
10
  Plugins
11
11
  `),e(` Discovered: ${t.plugins.discovered}\n`),t.plugins.gaps.length>0){e(` Assembly: ❌ 检测到装配缺口
12
12
  `);for(let n of t.plugins.gaps)e(` ${n}\n`)}else e(` Assembly: ✅
13
13
  `);if(i&&(e(`
14
14
  State
15
- `),e(` SQLite DB: ${i.dbPath}\n`),i.dbExists?(e(` size: ${A(i.dbSize??0)}\n`),e(` integrity: ${i.integrity===`ok`?`ok ✅`:i.integrity??`N/A`}\n`),i.snapshots!==void 0&&e(` snapshots: ${i.snapshots}\n`),i.changeLogEntries!==void 0&&e(` change_log: ${i.changeLogEntries} entries\n`),i.sessions!==void 0&&e(` sessions: ${i.sessions}\n`),i.walEnabled!==void 0&&e(` WAL: ${i.walEnabled?`enabled`:`disabled`}\n`)):e(` (no storage.db found — start otto to create one)
15
+ `),e(` SQLite DB: ${i.dbPath}\n`),i.dbExists?(e(` size: ${j(i.dbSize??0)}\n`),e(` integrity: ${i.integrity===`ok`?`ok ✅`:i.integrity??`N/A`}\n`),i.snapshots!==void 0&&e(` snapshots: ${i.snapshots}\n`),i.changeLogEntries!==void 0&&e(` change_log: ${i.changeLogEntries} entries\n`),i.sessions!==void 0&&e(` sessions: ${i.sessions}\n`),i.walEnabled!==void 0&&e(` WAL: ${i.walEnabled?`enabled`:`disabled`}\n`)):e(` (no storage.db found — start otto to create one)
16
16
  `)),e(`
17
17
  Connectivity
18
18
  `),e(` ${a.providerHost}: ${a.providerReachable?`reachable (${a.providerLatency}ms) ✅`:`unreachable ❌`}\n`),e(` storage host: ${a.storageHostConfigured?`configured`:`not configured (local only)`}\n`),t.ftue){let n=t.ftue.status===`looks-configured`?`✅`:`⚠`;e(`
19
19
  First-time setup
20
20
  `),e(` ${n} ${t.ftue.summary}\n`),e(` → ${t.ftue.recoveryAction}\n`)}e(`
21
- `)}function k(e){e(`otto doctor — 环境健康诊断
21
+ `)}function A(e){e(`otto doctor — 环境健康诊断
22
22
 
23
23
  用法:
24
24
  otto doctor [--summary|--json]
@@ -32,5 +32,5 @@ First-time setup
32
32
  otto doctor
33
33
  otto doctor --summary
34
34
  otto doctor --json
35
- `)}function A(e){if(e===0)return`0 B`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/1024**n).toFixed(1)} ${t[n]}`}export{_ as runDoctorCommand};
36
- //# sourceMappingURL=doctor-lT2QhG2q.js.map
35
+ `)}function j(e){if(e===0)return`0 B`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/1024**n).toFixed(1)} ${t[n]}`}export{v as runDoctorCommand};
36
+ //# sourceMappingURL=doctor-DXBLkbkP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-DXBLkbkP.js","names":[],"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { homedir, release, platform, arch, cpus, totalmem, freemem } from 'node:os'\nimport { existsSync, statSync, accessSync, constants } from 'node:fs'\nimport { join } from 'node:path'\nimport {\n SESSION_TOTAL_RESIDENCY_BUDGET,\n SESSION_MIN_PER_SESSION_BYTES,\n SESSION_MAX_PER_SESSION_BYTES,\n SESSION_MAX_HISTORY_MESSAGES,\n SESSION_RESUME_WINDOW_MESSAGES,\n} from '@x-otto/session-contract'\nexport interface DoctorDeps {\n stdout: (s: string) => void\n stderr: (s: string) => void\n}\n\nexport interface DoctorReport {\n environment: EnvironmentSection\n configuration: ConfigurationSection\n state?: StateSection\n connectivity: ConnectivitySection\n /** RFC-364 M2:插件装配一致性——发现得到却未装载的缺口。 */\n plugins?: PluginsSection\n /** RFC-326 M6-01:FTUE 环境自检(廉价事实推导,非权威 readiness 判定,见模块头注释)。 */\n ftue?: import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance\n}\n\ninterface PluginsSection {\n discovered: number\n /** 装配缺口的可执行诊断行(空数组 = 无缺口)。 */\n gaps: string[]\n /** 缺口涉及的插件 id(供 --json 消费方精确处理)。 */\n missing: Array<{ id: string; scope?: string }>\n}\n\ninterface EnvironmentSection {\n runtime: string\n installPath: string\n shell: string\n terminal: string\n cpu: string\n memory: { total: number; free: number }\n platform: string\n release: string\n}\n\ninterface ConfigurationSection {\n workspaceDir: string\n storageDir: string\n hasRipgrep: boolean\n hasGit: boolean\n authMode: string\n /** RFC-323:驻留预算配置(只读展示,使基线估算偏差可被校准) */\n residencyBudget?: {\n totalBytes: number\n minPerSessionBytes: number\n maxPerSessionBytes: number\n historyMessages: number\n resumeWindowMessages: number\n }\n}\n\ninterface StateSection {\n dbPath: string\n dbExists: boolean\n dbSize?: number\n walEnabled?: boolean\n snapshots?: number\n changeLogEntries?: number\n sessions?: number\n integrity?: string\n}\n\ninterface ConnectivitySection {\n providerHost: string\n providerReachable: boolean\n providerLatency?: number\n storageHostConfigured: boolean\n}\n\nconst OTTO_DIR = join(homedir(), '.otto')\n\nexport async function runDoctorCommand(\n args: string[],\n deps: DoctorDeps,\n): Promise<number> {\n const stdout = deps.stdout\n const stderr = deps.stderr\n\n // RFC-200 D1.0:子命令路由——首个位置参数(非 -- 开头)分发到诊断子命令;\n // 无子命令时走既有健康检查(行为零变化)。\n const sub = args.find((a) => !a.startsWith('-'))\n if (sub === 'ps' || sub === 'attach' || sub === 'heap') {\n const rest = args.filter((a) => a !== sub)\n switch (sub) {\n case 'ps': {\n const { runDoctorPs } = await import('./handlers/doctor-ps')\n return runDoctorPs(rest, deps)\n }\n case 'attach': {\n const { runDoctorAttach } = await import('./handlers/doctor-attach')\n return runDoctorAttach(rest, deps)\n }\n case 'heap': {\n const { runDoctorHeap } = await import('./handlers/doctor-heap')\n return runDoctorHeap(rest, deps)\n }\n }\n }\n if (sub !== undefined) {\n stderr(`otto doctor: unknown subcommand \"${sub}\" (available: ps, attach, heap)\\n`)\n return 1\n }\n\n if (args.includes('--help') || args.includes('-h')) {\n printHelp(stdout)\n return 0\n }\n\n const outputFormat = args.includes('--json') ? 'json' : args.includes('--summary') ? 'summary' : 'detailed'\n\n try {\n const report = await collectReport()\n\n if (outputFormat === 'json') {\n stdout(JSON.stringify(report, null, 2) + '\\n')\n } else if (outputFormat === 'summary') {\n printSummary(stdout, report)\n } else {\n printDetailed(stdout, report)\n }\n\n return 0\n } catch (error) {\n stderr(`otto doctor: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n\n/** 收集结构化诊断报告(TUI /doctor 折叠档复用;不打印)。 */\nexport async function collectReport(): Promise<DoctorReport> {\n const env = await collectEnvironment()\n const config = await collectConfiguration()\n const state = await collectState()\n const connectivity = await collectConnectivity()\n const { plugins, providerPluginDiscovered } = await collectPluginsAndProviderFlag()\n const ftue = await collectFtueGuidance(providerPluginDiscovered)\n\n return { environment: env, configuration: config, state, connectivity, plugins, ftue }\n}\n\nasync function collectEnvironment(): Promise<EnvironmentSection> {\n return {\n runtime: `Node.js ${process.version}`,\n installPath: process.execPath,\n shell: process.env.SHELL ?? '(unknown)',\n terminal: process.env.TERM ?? '(unknown)',\n cpu: `${cpus().length}×${arch()}`,\n memory: { total: totalmem(), free: freemem() },\n platform: platform(),\n release: release(),\n }\n}\n\nasync function collectConfiguration(): Promise<ConfigurationSection> {\n const authMode = process.env.ANTHROPIC_API_KEY\n ? 'api-key (ANTHROPIC_API_KEY)'\n : process.env.OTTO_API_KEY\n ? 'api-key (OTTO_API_KEY)'\n : process.env.AWS_ACCESS_KEY_ID\n ? 'aws-bedrock (env keys)'\n : 'not configured'\n\n return {\n workspaceDir: process.cwd(),\n storageDir: OTTO_DIR,\n hasRipgrep: await checkBinary('rg'),\n hasGit: await checkBinary('git'),\n authMode,\n // RFC-323 M3:暴露驻留预算配置,使\"基线 1500MB 估算偏差\"(R-2)可被真机数据校准。\n // 纯静态读常量,不接运行时状态——运行时有效分配值取决于活跃会话数 N,在 ps/doctor-ps 展示。\n residencyBudget: {\n totalBytes: SESSION_TOTAL_RESIDENCY_BUDGET,\n minPerSessionBytes: SESSION_MIN_PER_SESSION_BYTES,\n maxPerSessionBytes: SESSION_MAX_PER_SESSION_BYTES,\n historyMessages: SESSION_MAX_HISTORY_MESSAGES,\n resumeWindowMessages: SESSION_RESUME_WINDOW_MESSAGES,\n },\n }\n}\n\n/**\n * RFC-364 M2:插件装配一致性自检。\n *\n * 判据是「cli 侧完整发现(含 bundled 层)」与「App 实际装载」的差集。但 doctor 是\n * 轻量诊断命令(RFC-150 D3 明确要求不承担重依赖),构造真实 App 代价过高——\n * 故此处只做**发现侧**的自检:确认 bundled 目录可解析且其中的扩展能被发现。\n *\n * 真正的「发现 vs 装载」全量比对由运行时侧承担(App 启动路径),本处提供的是\n * 用户可主动运行的诊断入口:当用户报告\"插件不见了\"时,`otto doctor` 能立刻回答\n * \"otto 这一侧到底看见了哪些\"。\n */\n/**\n * RFC-326 M6-01:与 `providerPluginDiscovered` 标志合并计算——`discoverPluginsWithBundled`\n * 是本函数唯一的重量级调用,`collectFtueGuidance` 需要同一份发现结果的 `provider`\n * capability 过滤,若各自重新扫描一次会重复触发文件系统遍历。`providerPluginDiscovered`\n * 在扫描失败时缺省 `false`(诊断失败不阻断其余部分,与既有 fail-soft 语义一致;\n * FTUE 分支据此会诚实报告\"未发现 provider 插件\"而非静默跳过整个 ftue 字段)。\n */\nasync function collectPluginsAndProviderFlag(): Promise<{\n plugins: PluginsSection | undefined\n providerPluginDiscovered: boolean\n}> {\n try {\n const { discoverPluginsWithBundled } = await import('../discover-with-bundled')\n const { hostSelfDescribingOptions } = await import('../create-host-app')\n const { checkPluginAssembly, formatAssemblyGap } = await import('../plugin-assembly-check')\n\n const discovered = discoverPluginsWithBundled({ cwd: process.cwd(), homedir: homedir() })\n const bundledDirs = hostSelfDescribingOptions().pluginBundledDirs ?? []\n const providerPluginDiscovered = discovered.some((p) => p.manifest.capabilities?.includes('provider'))\n\n // 宿主自描述参数解析不出目录,但发现层却报告了 bundled 作用域插件(或反之)——\n // 这正是 M1 修的那类装配漂移,在此处显式暴露。\n const bundledSeen = discovered.filter((p) => p.scope === 'bundled')\n const assemblyBroken = bundledSeen.length > 0 && bundledDirs.length === 0\n\n const result = checkPluginAssembly({\n discovered: discovered.map((p) => ({ id: p.id, scope: p.scope })),\n // 发现侧自检:把「宿主参数解析失败」建模为\"全部 bundled 未装载\"\n active: assemblyBroken\n ? discovered.filter((p) => p.scope !== 'bundled').map((p) => ({ id: p.id, scope: p.scope }))\n : discovered.map((p) => ({ id: p.id, scope: p.scope })),\n })\n\n return {\n plugins: {\n discovered: discovered.length,\n gaps: formatAssemblyGap(result),\n missing: result.missingFromRuntime,\n },\n providerPluginDiscovered,\n }\n } catch {\n // 诊断失败不阻断 doctor 其余部分(与既有 collectState 的 fail-soft 一致)。\n return { plugins: undefined, providerPluginDiscovered: false }\n }\n}\n\n/**\n * RFC-326 M6-01:FTUE 环境自检——三个廉价事实推导诊断状态(见\n * `ftue-doctor-recovery.ts` 头注释的判据边界)。`AuthStore` 构造零 App 依赖\n * (默认读全局 `AUTH_PATH`),`listProviders()` 只读本地文件/env,不发网络请求。\n */\nasync function collectFtueGuidance(\n providerPluginDiscovered: boolean,\n): Promise<import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance | undefined> {\n try {\n const { createDefaultAuthStore } = await import('@x-otto/ai')\n const { deriveDoctorFtueGuidance } = await import('./ftue/ftue-doctor-recovery')\n const { SettingsManager } = await import('@x-otto/setting')\n const { OTTO_CONFIG_FILENAME } = await import('@x-otto/env')\n\n // watchForExternalChanges:false —— doctor 是一次性命令,跑完即退出,不需要\n // fs.watch(同 auth-command.ts 的既有约定,见其头注释)。\n const authStore = createDefaultAuthStore({ watchForExternalChanges: false })\n // listProviders() 读内存缓存(this.cache),不像 getAuthSource/hasCredential 那样\n // 自带 ensureInitialized() 前置——首次调用前必须显式触发一次磁盘加载,否则读到空态。\n await authStore.ensureInitialized()\n const hasAnyCredential = authStore.listProviders().length > 0\n\n const settings = new SettingsManager({ projectConfigPath: join(OTTO_DIR, OTTO_CONFIG_FILENAME) })\n await settings.load()\n const firstPromptSubmitted = typeof settings.get('first_prompt_submitted_at') === 'number'\n\n return deriveDoctorFtueGuidance({ providerPluginDiscovered, hasAnyCredential, firstPromptSubmitted })\n } catch {\n // 诊断失败不阻断 doctor 其余部分。\n return undefined\n }\n}\n\nasync function collectState(): Promise<StateSection | undefined> {\n const dbPath = join(OTTO_DIR, 'storage.db')\n if (!existsSync(dbPath)) {\n return { dbPath, dbExists: false }\n }\n\n try {\n const { SqlitePersistence } = await import('@x-otto/persistence')\n const store = new SqlitePersistence({ dbPath, namespace: 'doctor', wal: false })\n try {\n const info = store.getInfo() as {\n walEnabled: boolean\n snapshots: number\n changeLogEntries: number\n sessions: number\n integrity: string\n }\n\n return {\n dbPath,\n dbExists: true,\n dbSize: getFileSizeSync(dbPath),\n ...info,\n }\n } finally {\n store.close()\n }\n } catch {\n return { dbPath, dbExists: true, integrity: 'error (unable to open)' }\n }\n}\n\nasync function collectConnectivity(): Promise<ConnectivitySection> {\n let providerHost = 'api.anthropic.com'\n let providerReachable = false\n let providerLatency: number | undefined\n\n const start = Date.now()\n try {\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), 5000)\n await fetch(`https://${providerHost}`, {\n method: 'HEAD',\n signal: controller.signal,\n })\n clearTimeout(timeout)\n providerReachable = true\n providerLatency = Date.now() - start\n } catch {\n providerReachable = false\n }\n\n return {\n providerHost,\n providerReachable,\n providerLatency,\n storageHostConfigured: false,\n }\n}\n\n/**\n * 二进制探测(08-13 backlog triage 修复):此前只扫 /usr/local/bin 与 /usr/bin——\n * Homebrew 原生(/opt/homebrew/bin)、nix(~/.nix-profile/bin)等 PATH 布局被误报缺失。\n * 改为遍历 process.env.PATH 拆分项逐一探测(X_OK 替换魔数 1),并保留两个系统目录\n * 作为 PATH 未含时的兜底(部分守护进程环境 PATH 极简)。\n */\nexport async function checkBinary(name: string): Promise<boolean> {\n const dirs = new Set<string>()\n for (const p of (process.env['PATH'] ?? '').split(':')) {\n if (p) dirs.add(p)\n }\n dirs.add('/usr/local/bin')\n dirs.add('/usr/bin')\n for (const dir of dirs) {\n try {\n accessSync(join(dir, name), constants.X_OK)\n return true\n } catch {\n // 目录不存在或无可执行位——继续下一个候选。\n }\n }\n return false\n}\n\nfunction getFileSizeSync(path: string): number {\n try {\n return statSync(path).size\n } catch {\n return 0\n }\n}\n\nfunction printSummary(stdout: (s: string) => void, report: DoctorReport): void {\n const { environment: env, configuration: config, state, connectivity: conn } = report\n const dbOk = state?.dbExists && state.integrity === 'ok' ? '✅' : '❌'\n const netOk = conn.providerReachable ? '✅' : '❌'\n const pluginGap = report.plugins && report.plugins.gaps.length > 0\n ? ` ⚠ 插件装配缺口 ${report.plugins.missing.length} 项(otto doctor 查看详情)`\n : ''\n // RFC-326 M6-01:只在未配置(非 looks-configured)时提示——已配置用户不需要看\n // 这行噪音,未配置用户能立刻看到下一步命令,不必翻到 detailed 输出。\n const ftueHint = report.ftue && report.ftue.status !== 'looks-configured'\n ? ` ⚠ ${report.ftue.summary} → ${report.ftue.recoveryAction}`\n : ''\n stdout([\n `otto doctor — ${env.runtime} | ${config.storageDir} | DB:${dbOk} ${netOk}`,\n state ? ` ${state.snapshots ?? '?'} snapshots, ${state.changeLogEntries ?? '?'} change_log` : '',\n pluginGap,\n ftueHint,\n ].filter(Boolean).join('\\n') + '\\n')\n}\n\nfunction printDetailed(stdout: (s: string) => void, report: DoctorReport): void {\n const { environment: env, configuration: config, state, connectivity: conn } = report\n\n stdout('\\nEnvironment\\n')\n stdout(` Runtime: ${env.runtime}\\n`)\n stdout(` Install: ${env.installPath}\\n`)\n stdout(` Shell: ${env.shell}\\n`)\n stdout(` Terminal: ${env.terminal} (${env.release})\\n`)\n stdout(` CPU: ${env.cpu}\\n`)\n stdout(` Memory: ${formatBytes(env.memory.free)} / ${formatBytes(env.memory.total)} free\\n`)\n if (config.residencyBudget) {\n const rb = config.residencyBudget\n stdout(` Residency: budget=${formatBytes(rb.totalBytes)} per-session=${formatBytes(rb.minPerSessionBytes)}~${formatBytes(rb.maxPerSessionBytes)} history=${rb.historyMessages} resume=${rb.resumeWindowMessages}\\n`)\n }\n\n stdout('\\nConfiguration\\n')\n stdout(` Workspace: ${config.workspaceDir}\\n`)\n stdout(` Storage: ${config.storageDir} ${existsSync(config.storageDir) ? '✅' : '❌'}\\n`)\n stdout(` ripgrep: ${config.hasRipgrep ? '✅' : '❌'}\\n`)\n stdout(` git: ${config.hasGit ? '✅' : '❌'}\\n`)\n stdout(` Auth: ${config.authMode}\\n`)\n\n // RFC-364 M2:插件装配一致性——有缺口才打印(无缺口时不制造噪音)。\n if (report.plugins) {\n stdout('\\nPlugins\\n')\n stdout(` Discovered: ${report.plugins.discovered}\\n`)\n if (report.plugins.gaps.length > 0) {\n stdout(' Assembly: ❌ 检测到装配缺口\\n')\n for (const line of report.plugins.gaps) stdout(` ${line}\\n`)\n } else {\n stdout(' Assembly: ✅\\n')\n }\n }\n\n if (state) {\n stdout('\\nState\\n')\n stdout(` SQLite DB: ${state.dbPath}\\n`)\n if (state.dbExists) {\n stdout(` size: ${formatBytes(state.dbSize ?? 0)}\\n`)\n stdout(` integrity: ${state.integrity === 'ok' ? 'ok ✅' : state.integrity ?? 'N/A'}\\n`)\n if (state.snapshots !== undefined) stdout(` snapshots: ${state.snapshots}\\n`)\n if (state.changeLogEntries !== undefined) stdout(` change_log: ${state.changeLogEntries} entries\\n`)\n if (state.sessions !== undefined) stdout(` sessions: ${state.sessions}\\n`)\n if (state.walEnabled !== undefined) stdout(` WAL: ${state.walEnabled ? 'enabled' : 'disabled'}\\n`)\n } else {\n stdout(` (no storage.db found — start otto to create one)\\n`)\n }\n }\n\n stdout('\\nConnectivity\\n')\n stdout(` ${conn.providerHost}: ${conn.providerReachable ? `reachable (${conn.providerLatency}ms) ✅` : 'unreachable ❌'}\\n`)\n stdout(` storage host: ${conn.storageHostConfigured ? 'configured' : 'not configured (local only)'}\\n`)\n\n // RFC-326 M6-01:FTUE 环境自检——廉价事实推导,非权威 readiness 判定\n // (见 ftue-doctor-recovery.ts 头注释;权威判定只在 TUI 运行时的 FtueController)。\n if (report.ftue) {\n const icon = report.ftue.status === 'looks-configured' ? '✅' : '⚠'\n stdout('\\nFirst-time setup\\n')\n stdout(` ${icon} ${report.ftue.summary}\\n`)\n stdout(` → ${report.ftue.recoveryAction}\\n`)\n }\n\n stdout('\\n')\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto doctor — 环境健康诊断\n\n用法:\n otto doctor [--summary|--json]\n\n选项:\n --summary 紧凑输出(一行摘要)\n --json 结构化 JSON(redacted)\n --help, -h 帮助信息\n\n示例:\n otto doctor\n otto doctor --summary\n otto doctor --json\n`)\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n const i = Math.floor(Math.log(bytes) / Math.log(1024))\n return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-K_mIFRfA.js";import{execFileSync as o}from"node:child_process";async function s(o,s){let f=o.filter(e=>!e.startsWith(`-`)),p=Number(f[0]);if(!Number.isFinite(p)||p<=0)return s.stderr(`用法: otto doctor attach <pid> [--fix-user-timing]
2
+ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-CnJUVNOB.js";import{execFileSync as o}from"node:child_process";async function s(o,s){let f=o.filter(e=>!e.startsWith(`-`)),p=Number(f[0]);if(!Number.isFinite(p)||p<=0)return s.stderr(`用法: otto doctor attach <pid> [--fix-user-timing]
3
3
  (pid 可用 otto doctor ps 查看)
4
4
  `),1;try{process.kill(p,0)}catch{return s.stderr(`otto doctor attach: pid=${p} 不存在或无权限\n`),1}try{r()}catch(e){return s.stderr(`${e instanceof Error?e.message:String(e)}\n`),1}let m=t();if(m!=null&&m!==p)return s.stderr(`inspector 端口被 pid=${m} 占用,同一时刻只能 CDP attach 一个进程。\n`),s.stdout(`自动降级为 macOS 原生工具输出:
5
5
 
@@ -39,4 +39,4 @@ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-K_mIFRfA.js";impor
39
39
  `:[...e.entries()].sort((e,t)=>t[1]-e[1]).map(([e,t])=>` ${e}: ${t}`).join(`
40
40
  `)+`
41
41
  `}function d(e){return`${(e/(1024*1024)).toFixed(1)}MB`}export{s as runDoctorAttach};
42
- //# sourceMappingURL=doctor-attach-_zeIJewx.js.map
42
+ //# sourceMappingURL=doctor-attach-DEMuyBOk.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-attach-_zeIJewx.js","names":[],"sources":["../src/commands/handlers/doctor-attach.ts"],"sourcesContent":["/**\n * doctor-attach.ts — 安全附加 + 预置诊断查询(RFC-200 D1 / T200-2)。\n *\n * lesson_69/85/92 三次事故的手搓 CDP 配方机械化:\n * 端口预检 → USR1 → 身份核对 → 预置查询(active handles / user-timing / heap 统计 / worker 列表)。\n * 只读红线:唯一治理动作是显式 `--fix-user-timing`(lesson_92 已验证 clearMeasures 安全性)。\n * 9229 被占时自动降级 macOS 原生工具配方输出(不是只给提示退出——评审修订 4)。\n */\nimport { execFileSync } from 'node:child_process'\n\nimport {\n CdpSession,\n assertBuiltinWebSocket,\n inspectorPortOwner,\n nativeFallbackHint,\n openInspector,\n verifyTargetIdentity,\n} from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\nexport async function runDoctorAttach(args: string[], deps: DoctorDeps): Promise<number> {\n const positional = args.filter((a) => !a.startsWith('-'))\n const pid = Number(positional[0])\n if (!Number.isFinite(pid) || pid <= 0) {\n deps.stderr('用法: otto doctor attach <pid> [--fix-user-timing]\\n(pid 可用 otto doctor ps 查看)\\n')\n return 1\n }\n\n try {\n process.kill(pid, 0) // 存活探测(不发信号)\n } catch {\n deps.stderr(`otto doctor attach: pid=${pid} 不存在或无权限\\n`)\n return 1\n }\n\n try {\n assertBuiltinWebSocket()\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n // ① 端口预检(lesson_69 第一步)。被占 → 自动降级原生工具输出(评审修订 4)。\n const owner = inspectorPortOwner()\n if (owner != null && owner !== pid) {\n deps.stderr(`inspector 端口被 pid=${owner} 占用,同一时刻只能 CDP attach 一个进程。\\n`)\n deps.stdout('自动降级为 macOS 原生工具输出:\\n\\n')\n return runNativeFallback(pid, deps)\n }\n if (owner === null) {\n deps.stdout('(lsof 不可用,跳过端口预检——若下方身份核对失败请手动排查端口占用)\\n')\n }\n\n const session = new CdpSession()\n try {\n // ② USR1 开 inspector + ③ 身份核对(lesson_69 第二/三步)\n const target = await openInspector(pid)\n verifyTargetIdentity(target, pid)\n await session.connect(target.webSocketDebuggerUrl)\n deps.stdout(`已附加 pid=${pid}(${target.title})\\n\\n`)\n\n // 预置查询 1:active handles/resources(lesson_85 僵尸进程钉死配方)\n const handles = await session.evaluate<{ handles: string[]; resources: string[] }>(\n `(() => { const p = process; return {\n handles: (p._getActiveHandles?.() ?? []).map((h) => h?.constructor?.name ?? 'unknown'),\n resources: (p.getActiveResourcesInfo?.() ?? []),\n } })()`,\n )\n const handleCounts = countBy(handles.handles)\n deps.stdout(`— Active handles(事件循环 pinner,lesson_85)—\\n${fmtCounts(handleCounts)}\\n`)\n deps.stdout(`— Active resources —\\n${fmtCounts(countBy(handles.resources))}\\n`)\n\n // 预置查询 2:user-timing 缓冲(lesson_92 React dev-mode 2GB 驻留配方)\n const userTiming = await session.evaluate<{ measures: number; marks: number; nodeEnv: string }>(\n `(() => ({\n measures: performance.getEntriesByType('measure').length,\n marks: performance.getEntriesByType('mark').length,\n nodeEnv: process.env.NODE_ENV ?? '(unset)',\n }))()`,\n )\n deps.stdout(\n `— User-timing 缓冲(lesson_92)—\\nmeasures: ${userTiming.measures} marks: ${userTiming.marks} NODE_ENV: ${userTiming.nodeEnv}\\n`,\n )\n if (userTiming.measures > 100_000) {\n deps.stdout(\n `⚠ measures ${userTiming.measures} 条异常累积(lesson_92 事故特征:React dev 构建 user-timing 无界缓冲)。\\n` +\n ` 建议:otto doctor attach ${pid} --fix-user-timing 清空缓冲(已验证安全),并核查 NODE_ENV 未设为 production 的原因。\\n`,\n )\n }\n\n // 预置查询 3:heap 统计\n const heap = await session.evaluate<{ heapUsedMb: number; heapLimitMb: number; externalMb: number; rssMb: number }>(\n `(() => { const m = process.memoryUsage(); const v8 = require('v8').getHeapStatistics(); return {\n heapUsedMb: Math.round(m.heapUsed / 1048576),\n heapLimitMb: Math.round(v8.heap_size_limit / 1048576),\n externalMb: Math.round(m.external / 1048576),\n rssMb: Math.round(m.rss / 1048576),\n } })()`,\n )\n deps.stdout(\n `— Heap —\\nheapUsed: ${heap.heapUsedMb}MB / limit ${heap.heapLimitMb}MB external: ${heap.externalMb}MB rss: ${heap.rssMb}MB\\n`,\n )\n\n // 预置查询 4:RFC-325 M5 全局驻留快照。桥只暴露聚合数字,缺席时降级为未初始化。\n const residency = await session.evaluate<{\n available: boolean\n activeSessionCount?: number\n totalResidentBytes?: number\n globalPressure?: number\n isGlobalPressure?: boolean\n maxBytesPerSession?: number\n pressureLevel?: string\n }>(\n `(() => { const d = globalThis.__ottoResidencyDiagnostics; return typeof d?.getResidency === 'function'\n ? { available: true, ...d.getResidency() }\n : { available: false } })()`,\n )\n if (residency.available) {\n deps.stdout(\n `— Residency(RFC-325)—\\nactive: ${residency.activeSessionCount} resident: ${formatMb(residency.totalResidentBytes!)} ` +\n `pressure: ${Math.round(residency.globalPressure! * 100)}% per-session: ${formatMb(residency.maxBytesPerSession!)} ` +\n `level: ${residency.pressureLevel}${residency.isGlobalPressure ? ' ⚠ GLOBAL' : ''}\\n`,\n )\n } else {\n deps.stdout('— Residency(RFC-325)—\\n(unavailable: target has no running App diagnostics bridge)\\n')\n }\n\n // 预置查询 5:worker 线程列表(lesson_85 的 NodeWorker 归属定位,经 module 层枚举)\n const workers = await session.evaluate<string[]>(\n `(() => { try {\n const { Worker } = require('worker_threads')\n void Worker\n // 无公开 API 枚举 worker——用 process._getActiveHandles 里的 Worker 计数替代(保守但零侵入)\n return (process._getActiveHandles?.() ?? [])\n .filter((h) => h?.constructor?.name === 'Worker' || h?.constructor?.name === 'MessagePort')\n .map((h) => h.constructor.name)\n } catch { return [] } })()`,\n )\n deps.stdout(`— Worker/MessagePort handles —\\n${workers.length === 0 ? '(none)' : fmtCounts(countBy(workers))}\\n`)\n\n // 显式 opt-in 治理动作(唯一例外,lesson_92 验证过安全性)\n if (args.includes('--fix-user-timing')) {\n const cleared = await session.evaluate<number>(\n `(() => { const n = performance.getEntriesByType('measure').length; performance.clearMeasures(); performance.clearMarks(); return n })()`,\n )\n deps.stdout(`\\n--fix-user-timing:已清空 ${cleared} 条 measures(及全部 marks)。\\n`)\n }\n\n return 0\n } catch (error) {\n deps.stderr(`otto doctor attach 失败: ${error instanceof Error ? error.message : String(error)}\\n`)\n deps.stdout('\\n降级配方:\\n' + nativeFallbackHint(pid) + '\\n')\n return 1\n } finally {\n // 风险 3:退出即断开;目标进程的 inspector 由其自身生命周期管理(我们不主动 close 它的\n // server——那需要目标进程内执行 inspector.close(),超出只读边界;端口在断连后可复用)。\n session.close()\n }\n}\n\nfunction runNativeFallback(pid: number, deps: DoctorDeps): number {\n deps.stdout(nativeFallbackHint(pid) + '\\n\\n')\n try {\n // sample 5 秒采样输出裁剪到调用栈头部——等价于手动配方的第一步\n const out = execFileSync('sample', [String(pid), '2', '-file', '/dev/stdout'], {\n encoding: 'utf-8',\n timeout: 15_000,\n maxBuffer: 8 * 1024 * 1024,\n })\n const head = out.split('\\n').slice(0, 40).join('\\n')\n deps.stdout(`— sample ${pid}(2s CPU 栈头部)—\\n${head}\\n`)\n return 0\n } catch {\n deps.stdout('(sample 不可用——非 macOS 或无权限;请按上方配方手动执行)\\n')\n return 1\n }\n}\n\nfunction countBy(items: string[]): Map<string, number> {\n const map = new Map<string, number>()\n for (const item of items) map.set(item, (map.get(item) ?? 0) + 1)\n return map\n}\n\nfunction fmtCounts(map: Map<string, number>): string {\n if (map.size === 0) return '(none)\\n'\n return [...map.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([name, count]) => ` ${name}: ${count}`)\n .join('\\n') + '\\n'\n}\n\n\nfunction formatMb(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"doctor-attach-DEMuyBOk.js","names":[],"sources":["../src/commands/handlers/doctor-attach.ts"],"sourcesContent":["/**\n * doctor-attach.ts — 安全附加 + 预置诊断查询(RFC-200 D1 / T200-2)。\n *\n * lesson_69/85/92 三次事故的手搓 CDP 配方机械化:\n * 端口预检 → USR1 → 身份核对 → 预置查询(active handles / user-timing / heap 统计 / worker 列表)。\n * 只读红线:唯一治理动作是显式 `--fix-user-timing`(lesson_92 已验证 clearMeasures 安全性)。\n * 9229 被占时自动降级 macOS 原生工具配方输出(不是只给提示退出——评审修订 4)。\n */\nimport { execFileSync } from 'node:child_process'\n\nimport {\n CdpSession,\n assertBuiltinWebSocket,\n inspectorPortOwner,\n nativeFallbackHint,\n openInspector,\n verifyTargetIdentity,\n} from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\nexport async function runDoctorAttach(args: string[], deps: DoctorDeps): Promise<number> {\n const positional = args.filter((a) => !a.startsWith('-'))\n const pid = Number(positional[0])\n if (!Number.isFinite(pid) || pid <= 0) {\n deps.stderr('用法: otto doctor attach <pid> [--fix-user-timing]\\n(pid 可用 otto doctor ps 查看)\\n')\n return 1\n }\n\n try {\n process.kill(pid, 0) // 存活探测(不发信号)\n } catch {\n deps.stderr(`otto doctor attach: pid=${pid} 不存在或无权限\\n`)\n return 1\n }\n\n try {\n assertBuiltinWebSocket()\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n // ① 端口预检(lesson_69 第一步)。被占 → 自动降级原生工具输出(评审修订 4)。\n const owner = inspectorPortOwner()\n if (owner != null && owner !== pid) {\n deps.stderr(`inspector 端口被 pid=${owner} 占用,同一时刻只能 CDP attach 一个进程。\\n`)\n deps.stdout('自动降级为 macOS 原生工具输出:\\n\\n')\n return runNativeFallback(pid, deps)\n }\n if (owner === null) {\n deps.stdout('(lsof 不可用,跳过端口预检——若下方身份核对失败请手动排查端口占用)\\n')\n }\n\n const session = new CdpSession()\n try {\n // ② USR1 开 inspector + ③ 身份核对(lesson_69 第二/三步)\n const target = await openInspector(pid)\n verifyTargetIdentity(target, pid)\n await session.connect(target.webSocketDebuggerUrl)\n deps.stdout(`已附加 pid=${pid}(${target.title})\\n\\n`)\n\n // 预置查询 1:active handles/resources(lesson_85 僵尸进程钉死配方)\n const handles = await session.evaluate<{ handles: string[]; resources: string[] }>(\n `(() => { const p = process; return {\n handles: (p._getActiveHandles?.() ?? []).map((h) => h?.constructor?.name ?? 'unknown'),\n resources: (p.getActiveResourcesInfo?.() ?? []),\n } })()`,\n )\n const handleCounts = countBy(handles.handles)\n deps.stdout(`— Active handles(事件循环 pinner,lesson_85)—\\n${fmtCounts(handleCounts)}\\n`)\n deps.stdout(`— Active resources —\\n${fmtCounts(countBy(handles.resources))}\\n`)\n\n // 预置查询 2:user-timing 缓冲(lesson_92 React dev-mode 2GB 驻留配方)\n const userTiming = await session.evaluate<{ measures: number; marks: number; nodeEnv: string }>(\n `(() => ({\n measures: performance.getEntriesByType('measure').length,\n marks: performance.getEntriesByType('mark').length,\n nodeEnv: process.env.NODE_ENV ?? '(unset)',\n }))()`,\n )\n deps.stdout(\n `— User-timing 缓冲(lesson_92)—\\nmeasures: ${userTiming.measures} marks: ${userTiming.marks} NODE_ENV: ${userTiming.nodeEnv}\\n`,\n )\n if (userTiming.measures > 100_000) {\n deps.stdout(\n `⚠ measures ${userTiming.measures} 条异常累积(lesson_92 事故特征:React dev 构建 user-timing 无界缓冲)。\\n` +\n ` 建议:otto doctor attach ${pid} --fix-user-timing 清空缓冲(已验证安全),并核查 NODE_ENV 未设为 production 的原因。\\n`,\n )\n }\n\n // 预置查询 3:heap 统计\n const heap = await session.evaluate<{ heapUsedMb: number; heapLimitMb: number; externalMb: number; rssMb: number }>(\n `(() => { const m = process.memoryUsage(); const v8 = require('v8').getHeapStatistics(); return {\n heapUsedMb: Math.round(m.heapUsed / 1048576),\n heapLimitMb: Math.round(v8.heap_size_limit / 1048576),\n externalMb: Math.round(m.external / 1048576),\n rssMb: Math.round(m.rss / 1048576),\n } })()`,\n )\n deps.stdout(\n `— Heap —\\nheapUsed: ${heap.heapUsedMb}MB / limit ${heap.heapLimitMb}MB external: ${heap.externalMb}MB rss: ${heap.rssMb}MB\\n`,\n )\n\n // 预置查询 4:RFC-325 M5 全局驻留快照。桥只暴露聚合数字,缺席时降级为未初始化。\n const residency = await session.evaluate<{\n available: boolean\n activeSessionCount?: number\n totalResidentBytes?: number\n globalPressure?: number\n isGlobalPressure?: boolean\n maxBytesPerSession?: number\n pressureLevel?: string\n }>(\n `(() => { const d = globalThis.__ottoResidencyDiagnostics; return typeof d?.getResidency === 'function'\n ? { available: true, ...d.getResidency() }\n : { available: false } })()`,\n )\n if (residency.available) {\n deps.stdout(\n `— Residency(RFC-325)—\\nactive: ${residency.activeSessionCount} resident: ${formatMb(residency.totalResidentBytes!)} ` +\n `pressure: ${Math.round(residency.globalPressure! * 100)}% per-session: ${formatMb(residency.maxBytesPerSession!)} ` +\n `level: ${residency.pressureLevel}${residency.isGlobalPressure ? ' ⚠ GLOBAL' : ''}\\n`,\n )\n } else {\n deps.stdout('— Residency(RFC-325)—\\n(unavailable: target has no running App diagnostics bridge)\\n')\n }\n\n // 预置查询 5:worker 线程列表(lesson_85 的 NodeWorker 归属定位,经 module 层枚举)\n const workers = await session.evaluate<string[]>(\n `(() => { try {\n const { Worker } = require('worker_threads')\n void Worker\n // 无公开 API 枚举 worker——用 process._getActiveHandles 里的 Worker 计数替代(保守但零侵入)\n return (process._getActiveHandles?.() ?? [])\n .filter((h) => h?.constructor?.name === 'Worker' || h?.constructor?.name === 'MessagePort')\n .map((h) => h.constructor.name)\n } catch { return [] } })()`,\n )\n deps.stdout(`— Worker/MessagePort handles —\\n${workers.length === 0 ? '(none)' : fmtCounts(countBy(workers))}\\n`)\n\n // 显式 opt-in 治理动作(唯一例外,lesson_92 验证过安全性)\n if (args.includes('--fix-user-timing')) {\n const cleared = await session.evaluate<number>(\n `(() => { const n = performance.getEntriesByType('measure').length; performance.clearMeasures(); performance.clearMarks(); return n })()`,\n )\n deps.stdout(`\\n--fix-user-timing:已清空 ${cleared} 条 measures(及全部 marks)。\\n`)\n }\n\n return 0\n } catch (error) {\n deps.stderr(`otto doctor attach 失败: ${error instanceof Error ? error.message : String(error)}\\n`)\n deps.stdout('\\n降级配方:\\n' + nativeFallbackHint(pid) + '\\n')\n return 1\n } finally {\n // 风险 3:退出即断开;目标进程的 inspector 由其自身生命周期管理(我们不主动 close 它的\n // server——那需要目标进程内执行 inspector.close(),超出只读边界;端口在断连后可复用)。\n session.close()\n }\n}\n\nfunction runNativeFallback(pid: number, deps: DoctorDeps): number {\n deps.stdout(nativeFallbackHint(pid) + '\\n\\n')\n try {\n // sample 5 秒采样输出裁剪到调用栈头部——等价于手动配方的第一步\n const out = execFileSync('sample', [String(pid), '2', '-file', '/dev/stdout'], {\n encoding: 'utf-8',\n timeout: 15_000,\n maxBuffer: 8 * 1024 * 1024,\n })\n const head = out.split('\\n').slice(0, 40).join('\\n')\n deps.stdout(`— sample ${pid}(2s CPU 栈头部)—\\n${head}\\n`)\n return 0\n } catch {\n deps.stdout('(sample 不可用——非 macOS 或无权限;请按上方配方手动执行)\\n')\n return 1\n }\n}\n\nfunction countBy(items: string[]): Map<string, number> {\n const map = new Map<string, number>()\n for (const item of items) map.set(item, (map.get(item) ?? 0) + 1)\n return map\n}\n\nfunction fmtCounts(map: Map<string, number>): string {\n if (map.size === 0) return '(none)\\n'\n return [...map.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([name, count]) => ` ${name}: ${count}`)\n .join('\\n') + '\\n'\n}\n\n\nfunction formatMb(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`\n}\n"],"mappings":"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"}
@@ -2,4 +2,4 @@
2
2
  import{execFileSync as e}from"node:child_process";const t=9229;function n(){if(typeof WebSocket!=`function`)throw Error(`otto doctor attach 需要 Node >= 22(内建 WebSocket);当前 ${process.version}。低版本可改用手动配方:kill -USR1 <pid> 后用 Chrome DevTools 连 chrome://inspect。`)}function r(){try{let n=e(`lsof`,[`-nP`,`-iTCP:${t}`,`-sTCP:LISTEN`,`-t`],{encoding:`utf-8`,timeout:5e3}).trim();if(!n)return;let r=Number(n.split(`
3
3
  `)[0]);return Number.isFinite(r)?r:void 0}catch(e){return e.status===1?void 0:null}}async function i(e,n=3e3){process.kill(e,`SIGUSR1`);let r=Date.now()+n,i;for(;Date.now()<r;){try{let e=(await(await fetch(`http://127.0.0.1:${t}/json`)).json()).find(e=>e.webSocketDebuggerUrl);if(e?.webSocketDebuggerUrl){let t=/\[(\d+)\]/.exec(e.title??``);return{webSocketDebuggerUrl:e.webSocketDebuggerUrl,title:e.title??``,pid:t?Number(t[1]):void 0}}}catch(e){i=e}await new Promise(e=>setTimeout(e,100))}throw Error(`inspector 未在 ${n}ms 内就绪(pid=${e}):${i instanceof Error?i.message:`目标未监听`}`)}function a(e,t){if(e.pid!==void 0&&e.pid!==t)throw Error(`9229 端口上的 inspector 属于 pid=${e.pid}("${e.title}"),非目标 pid=${t}——端口被其他进程占用,拒绝连接(lesson_69:误连他进程数据会污染分析)。`)}var o=class{ws;nextId=1;pending=new Map;async connect(e){this.ws=new WebSocket(e),await new Promise((e,t)=>{this.ws.addEventListener(`open`,()=>e(),{once:!0}),this.ws.addEventListener(`error`,()=>t(Error(`CDP WebSocket 连接失败`)),{once:!0})}),this.ws.addEventListener(`message`,e=>{try{let t=JSON.parse(String(e.data));if(t.id===void 0)return;let n=this.pending.get(t.id);if(!n)return;this.pending.delete(t.id),t.error?n.reject(Error(t.error.message)):n.resolve(t.result)}catch{}})}send(e,t){let n=this.nextId++,r=new Promise((t,r)=>{this.pending.set(n,{resolve:t,reject:r}),setTimeout(()=>{this.pending.delete(n)&&r(Error(`CDP ${e} 超时(10s)`))},1e4).unref?.()});return this.ws.send(JSON.stringify({id:n,method:e,params:t})),r}async evaluate(e){await this.send(`Runtime.enable`);let t=await this.send(`Runtime.evaluate`,{expression:e,returnByValue:!0,includeCommandLineAPI:!1});if(t.exceptionDetails)throw Error(`目标进程内求值异常: ${t.exceptionDetails.text}`);return t.result.value}close(){try{this.ws.close()}catch{}}};function s(e){return[`# 9229 已被占用或 CDP 不可用——macOS 原生工具等价配方(lesson_69):`,`sample ${e} 5 -file /dev/stdout | head -60 # CPU 调用栈(5s 采样)`,`vmmap --summary ${e} | head -30 # 内存分区(V8 heap 在 Memory Tag 255)`,`heap ${e} | head -40 # malloc zone 统计`].join(`
4
4
  `)}export{s as a,r as i,t as n,i as o,n as r,a as s,o as t};
5
- //# sourceMappingURL=doctor-cdp-K_mIFRfA.js.map
5
+ //# sourceMappingURL=doctor-cdp-CnJUVNOB.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-cdp-K_mIFRfA.js","names":[],"sources":["../src/commands/handlers/doctor-cdp.ts"],"sourcesContent":["/**\n * /doctor cdp 命令:CDP 探针(--url / --list-targets / --heap-sample / --evaluate)。\n */\nimport { execFileSync } from 'node:child_process'\n\nexport const INSPECTOR_PORT = 9229\nexport const INSPECTOR_HOST = '127.0.0.1'\n\nexport interface InspectorTarget {\n webSocketDebuggerUrl: string\n title: string\n /** /json 里的 title 通常含 pid(如 \"node[86152]\");此处解析出的 pid,解析失败为 undefined。 */\n pid?: number\n}\n\n/** Node ≥22 内建 WebSocket 存在性检查(启动前置,低版本给明确报错)。 */\nexport function assertBuiltinWebSocket(): void {\n if (typeof WebSocket !== 'function') {\n throw new Error(\n `otto doctor attach 需要 Node >= 22(内建 WebSocket);当前 ${process.version}。` +\n '低版本可改用手动配方:kill -USR1 <pid> 后用 Chrome DevTools 连 chrome://inspect。',\n )\n }\n}\n\n/** lsof 查 9229 端口占用者 pid;空闲返回 undefined。lsof 不可用(非 macOS/Linux)返回 null。 */\nexport function inspectorPortOwner(): number | undefined | null {\n try {\n const out = execFileSync('lsof', ['-nP', `-iTCP:${INSPECTOR_PORT}`, '-sTCP:LISTEN', '-t'], {\n encoding: 'utf-8',\n timeout: 5_000,\n }).trim()\n if (!out) return undefined\n const pid = Number(out.split('\\n')[0])\n return Number.isFinite(pid) ? pid : undefined\n } catch (error) {\n // lsof 无匹配时退出码 1(正常空闲);命令不存在等其他错误返回 null 让调用方降级。\n const status = (error as { status?: number }).status\n return status === 1 ? undefined : null\n }\n}\n\n/** 向目标进程发 SIGUSR1 打开 inspector,并轮询 /json 直到就绪(默认 3s 超时)。 */\nexport async function openInspector(pid: number, timeoutMs = 3_000): Promise<InspectorTarget> {\n process.kill(pid, 'SIGUSR1')\n const deadline = Date.now() + timeoutMs\n let lastError: unknown\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`http://${INSPECTOR_HOST}:${INSPECTOR_PORT}/json`)\n const targets = (await res.json()) as Array<{ webSocketDebuggerUrl?: string; title?: string }>\n const target = targets.find((t) => t.webSocketDebuggerUrl)\n if (target?.webSocketDebuggerUrl) {\n const pidMatch = /\\[(\\d+)\\]/.exec(target.title ?? '')\n return {\n webSocketDebuggerUrl: target.webSocketDebuggerUrl,\n title: target.title ?? '',\n pid: pidMatch ? Number(pidMatch[1]) : undefined,\n }\n }\n } catch (error) {\n lastError = error\n }\n await new Promise((resolve) => setTimeout(resolve, 100))\n }\n throw new Error(\n `inspector 未在 ${timeoutMs}ms 内就绪(pid=${pid}):${lastError instanceof Error ? lastError.message : '目标未监听'}`,\n )\n}\n\n/**\n * 身份核对(lesson_69 第三步):/json title 含 pid 时必须与目标一致——\n * 端口被其他进程(如 Electron --inspect)占用时误连的数据会完全误导分析。\n */\nexport function verifyTargetIdentity(target: InspectorTarget, expectedPid: number): void {\n if (target.pid !== undefined && target.pid !== expectedPid) {\n throw new Error(\n `9229 端口上的 inspector 属于 pid=${target.pid}(\"${target.title}\"),非目标 pid=${expectedPid}——` +\n '端口被其他进程占用,拒绝连接(lesson_69:误连他进程数据会污染分析)。',\n )\n }\n}\n\n/** 极简 CDP 会话:内建 WebSocket + 自增 id 的请求-响应配对。 */\nexport class CdpSession {\n private ws!: WebSocket\n private nextId = 1\n private readonly pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>()\n\n async connect(url: string): Promise<void> {\n this.ws = new WebSocket(url)\n await new Promise<void>((resolve, reject) => {\n this.ws.addEventListener('open', () => resolve(), { once: true })\n this.ws.addEventListener('error', () => reject(new Error('CDP WebSocket 连接失败')), { once: true })\n })\n this.ws.addEventListener('message', (event) => {\n try {\n const msg = JSON.parse(String(event.data)) as { id?: number; result?: unknown; error?: { message: string } }\n if (msg.id === undefined) return // 事件通知,当前工具不消费\n const entry = this.pending.get(msg.id)\n if (!entry) return\n this.pending.delete(msg.id)\n if (msg.error) entry.reject(new Error(msg.error.message))\n else entry.resolve(msg.result)\n } catch { /* 非 JSON 帧忽略 */ }\n })\n }\n\n send<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = this.nextId++\n const promise = new Promise<T>((resolve, reject) => {\n this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject })\n setTimeout(() => {\n if (this.pending.delete(id)) reject(new Error(`CDP ${method} 超时(10s)`))\n }, 10_000).unref?.()\n })\n this.ws.send(JSON.stringify({ id, method, params }))\n return promise\n }\n\n /** 在目标进程内求值表达式(returnByValue),返回结果值。 */\n async evaluate<T = unknown>(expression: string): Promise<T> {\n await this.send('Runtime.enable')\n const result = await this.send<{ result: { value?: T }; exceptionDetails?: { text: string } }>(\n 'Runtime.evaluate',\n { expression, returnByValue: true, includeCommandLineAPI: false },\n )\n if (result.exceptionDetails) throw new Error(`目标进程内求值异常: ${result.exceptionDetails.text}`)\n return result.result.value as T\n }\n\n close(): void {\n try { this.ws.close() } catch { /* 已断开 */ }\n }\n}\n\n/** macOS 原生工具降级路径(9229 被占/lsof 不可用时的等价输出,lesson_69 回退配方)。 */\nexport function nativeFallbackHint(pid: number): string {\n return [\n `# 9229 已被占用或 CDP 不可用——macOS 原生工具等价配方(lesson_69):`,\n `sample ${pid} 5 -file /dev/stdout | head -60 # CPU 调用栈(5s 采样)`,\n `vmmap --summary ${pid} | head -30 # 内存分区(V8 heap 在 Memory Tag 255)`,\n `heap ${pid} | head -40 # malloc zone 统计`,\n ].join('\\n')\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"doctor-cdp-CnJUVNOB.js","names":[],"sources":["../src/commands/handlers/doctor-cdp.ts"],"sourcesContent":["/**\n * /doctor cdp 命令:CDP 探针(--url / --list-targets / --heap-sample / --evaluate)。\n */\nimport { execFileSync } from 'node:child_process'\n\nexport const INSPECTOR_PORT = 9229\nexport const INSPECTOR_HOST = '127.0.0.1'\n\nexport interface InspectorTarget {\n webSocketDebuggerUrl: string\n title: string\n /** /json 里的 title 通常含 pid(如 \"node[86152]\");此处解析出的 pid,解析失败为 undefined。 */\n pid?: number\n}\n\n/** Node ≥22 内建 WebSocket 存在性检查(启动前置,低版本给明确报错)。 */\nexport function assertBuiltinWebSocket(): void {\n if (typeof WebSocket !== 'function') {\n throw new Error(\n `otto doctor attach 需要 Node >= 22(内建 WebSocket);当前 ${process.version}。` +\n '低版本可改用手动配方:kill -USR1 <pid> 后用 Chrome DevTools 连 chrome://inspect。',\n )\n }\n}\n\n/** lsof 查 9229 端口占用者 pid;空闲返回 undefined。lsof 不可用(非 macOS/Linux)返回 null。 */\nexport function inspectorPortOwner(): number | undefined | null {\n try {\n const out = execFileSync('lsof', ['-nP', `-iTCP:${INSPECTOR_PORT}`, '-sTCP:LISTEN', '-t'], {\n encoding: 'utf-8',\n timeout: 5_000,\n }).trim()\n if (!out) return undefined\n const pid = Number(out.split('\\n')[0])\n return Number.isFinite(pid) ? pid : undefined\n } catch (error) {\n // lsof 无匹配时退出码 1(正常空闲);命令不存在等其他错误返回 null 让调用方降级。\n const status = (error as { status?: number }).status\n return status === 1 ? undefined : null\n }\n}\n\n/** 向目标进程发 SIGUSR1 打开 inspector,并轮询 /json 直到就绪(默认 3s 超时)。 */\nexport async function openInspector(pid: number, timeoutMs = 3_000): Promise<InspectorTarget> {\n process.kill(pid, 'SIGUSR1')\n const deadline = Date.now() + timeoutMs\n let lastError: unknown\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`http://${INSPECTOR_HOST}:${INSPECTOR_PORT}/json`)\n const targets = (await res.json()) as Array<{ webSocketDebuggerUrl?: string; title?: string }>\n const target = targets.find((t) => t.webSocketDebuggerUrl)\n if (target?.webSocketDebuggerUrl) {\n const pidMatch = /\\[(\\d+)\\]/.exec(target.title ?? '')\n return {\n webSocketDebuggerUrl: target.webSocketDebuggerUrl,\n title: target.title ?? '',\n pid: pidMatch ? Number(pidMatch[1]) : undefined,\n }\n }\n } catch (error) {\n lastError = error\n }\n await new Promise((resolve) => setTimeout(resolve, 100))\n }\n throw new Error(\n `inspector 未在 ${timeoutMs}ms 内就绪(pid=${pid}):${lastError instanceof Error ? lastError.message : '目标未监听'}`,\n )\n}\n\n/**\n * 身份核对(lesson_69 第三步):/json title 含 pid 时必须与目标一致——\n * 端口被其他进程(如 Electron --inspect)占用时误连的数据会完全误导分析。\n */\nexport function verifyTargetIdentity(target: InspectorTarget, expectedPid: number): void {\n if (target.pid !== undefined && target.pid !== expectedPid) {\n throw new Error(\n `9229 端口上的 inspector 属于 pid=${target.pid}(\"${target.title}\"),非目标 pid=${expectedPid}——` +\n '端口被其他进程占用,拒绝连接(lesson_69:误连他进程数据会污染分析)。',\n )\n }\n}\n\n/** 极简 CDP 会话:内建 WebSocket + 自增 id 的请求-响应配对。 */\nexport class CdpSession {\n private ws!: WebSocket\n private nextId = 1\n private readonly pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>()\n\n async connect(url: string): Promise<void> {\n this.ws = new WebSocket(url)\n await new Promise<void>((resolve, reject) => {\n this.ws.addEventListener('open', () => resolve(), { once: true })\n this.ws.addEventListener('error', () => reject(new Error('CDP WebSocket 连接失败')), { once: true })\n })\n this.ws.addEventListener('message', (event) => {\n try {\n const msg = JSON.parse(String(event.data)) as { id?: number; result?: unknown; error?: { message: string } }\n if (msg.id === undefined) return // 事件通知,当前工具不消费\n const entry = this.pending.get(msg.id)\n if (!entry) return\n this.pending.delete(msg.id)\n if (msg.error) entry.reject(new Error(msg.error.message))\n else entry.resolve(msg.result)\n } catch { /* 非 JSON 帧忽略 */ }\n })\n }\n\n send<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = this.nextId++\n const promise = new Promise<T>((resolve, reject) => {\n this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject })\n setTimeout(() => {\n if (this.pending.delete(id)) reject(new Error(`CDP ${method} 超时(10s)`))\n }, 10_000).unref?.()\n })\n this.ws.send(JSON.stringify({ id, method, params }))\n return promise\n }\n\n /** 在目标进程内求值表达式(returnByValue),返回结果值。 */\n async evaluate<T = unknown>(expression: string): Promise<T> {\n await this.send('Runtime.enable')\n const result = await this.send<{ result: { value?: T }; exceptionDetails?: { text: string } }>(\n 'Runtime.evaluate',\n { expression, returnByValue: true, includeCommandLineAPI: false },\n )\n if (result.exceptionDetails) throw new Error(`目标进程内求值异常: ${result.exceptionDetails.text}`)\n return result.result.value as T\n }\n\n close(): void {\n try { this.ws.close() } catch { /* 已断开 */ }\n }\n}\n\n/** macOS 原生工具降级路径(9229 被占/lsof 不可用时的等价输出,lesson_69 回退配方)。 */\nexport function nativeFallbackHint(pid: number): string {\n return [\n `# 9229 已被占用或 CDP 不可用——macOS 原生工具等价配方(lesson_69):`,\n `sample ${pid} 5 -file /dev/stdout | head -60 # CPU 调用栈(5s 采样)`,\n `vmmap --summary ${pid} | head -30 # 内存分区(V8 heap 在 Memory Tag 255)`,\n `heap ${pid} | head -40 # malloc zone 统计`,\n ].join('\\n')\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-K_mIFRfA.js";async function o(o,s){let c=o.filter(e=>!e.startsWith(`-`)),l=Number(c[0]);if(!Number.isFinite(l)||l<=0)return s.stderr(`用法: otto doctor heap <pid> [--window <秒,默认 10>]
2
+ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-CnJUVNOB.js";async function o(o,s){let c=o.filter(e=>!e.startsWith(`-`)),l=Number(c[0]);if(!Number.isFinite(l)||l<=0)return s.stderr(`用法: otto doctor heap <pid> [--window <秒,默认 10>]
3
3
  `),1;let u=o.indexOf(`--window`),d=u===-1?10:Number(o[u+1])||10;try{r()}catch(e){return s.stderr(`${e instanceof Error?e.message:String(e)}\n`),1}let f=t();if(f!=null&&f!==l)return s.stderr(`inspector 端口被 pid=${f} 占用。\n${e(l)}\n`),1;let p=new a;try{let e=await n(l);i(e,l),await p.connect(e.webSocketDebuggerUrl),s.stdout(`采样中(${d}s 窗口,pid=${l})……\n`),await p.send(`HeapProfiler.enable`),await p.send(`HeapProfiler.startSampling`,{samplingInterval:16384}),await new Promise(e=>setTimeout(e,d*1e3));let{profile:t}=await p.send(`HeapProfiler.stopSampling`),r=new Map,a=e=>{if(e.selfSize>0){let t=`${e.callFrame.functionName||`(anonymous)`} @ ${e.callFrame.url.replace(/^file:\/\/.*\/(packages|node_modules)\//,`$1/`)||`(native)`}`;r.set(t,(r.get(t)??0)+e.selfSize)}for(let t of e.children??[])a(t)};a(t.head);let o=[...r.entries()].sort((e,t)=>t[1]-e[1]).slice(0,20),c=[...r.values()].reduce((e,t)=>e+t,0);s.stdout(`\n— Top 分配帧(采样窗口内共 ${(c/1048576).toFixed(1)}MB 采样分配)—\n`),s.stdout(`SIZE(MB) % FRAME
4
4
  `);for(let[e,t]of o){let n=(t/1048576).toFixed(2),r=c>0?(t/c*100).toFixed(1):`0.0`;s.stdout(`${n.padEnd(9)} ${(r+`%`).padEnd(6)} ${e}\n`)}return s.stdout(`
5
5
  判读提示(lesson_92):top 帧若是 measure @ node:internal/perf/performance → user-timing 无界缓冲
@@ -8,4 +8,4 @@ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-K_mIFRfA.js";async
8
8
  降级配方:
9
9
  `+e(l)+`
10
10
  `),1}finally{p.close()}}export{o as runDoctorHeap};
11
- //# sourceMappingURL=doctor-heap-Rr2nNvcw.js.map
11
+ //# sourceMappingURL=doctor-heap-Eu3LZj-F.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-heap-Rr2nNvcw.js","names":[],"sources":["../src/commands/handlers/doctor-heap.ts"],"sourcesContent":["/**\n * doctor-heap.ts — 短窗 heap 分配采样 + top 分配帧(RFC-200 D1 / T200-3)。\n *\n * lesson_92 方法论固化:「heap sampling 的 top 分配帧直接指向驻留源,比逐结构猜测快得多」\n * ——2GB user-timing 驻留正是靠 top 帧 `measure @ node:internal/perf/performance` 一击定位。\n * 流程:安全附加(复用 doctor-cdp 三步)→ HeapProfiler.startSampling → 等待采样窗口 →\n * stopSampling → 聚合调用树 → 按 selfSize 排序输出 top 帧。只读,不触发 GC。\n */\nimport {\n CdpSession,\n assertBuiltinWebSocket,\n inspectorPortOwner,\n nativeFallbackHint,\n openInspector,\n verifyTargetIdentity,\n} from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\ninterface SamplingNode {\n callFrame: { functionName: string; url: string }\n selfSize: number\n children?: SamplingNode[]\n}\n\nexport async function runDoctorHeap(args: string[], deps: DoctorDeps): Promise<number> {\n const positional = args.filter((a) => !a.startsWith('-'))\n const pid = Number(positional[0])\n if (!Number.isFinite(pid) || pid <= 0) {\n deps.stderr('用法: otto doctor heap <pid> [--window <秒,默认 10>]\\n')\n return 1\n }\n\n const windowIdx = args.indexOf('--window')\n const windowSec = windowIdx !== -1 ? Number(args[windowIdx + 1]) || 10 : 10\n\n try {\n assertBuiltinWebSocket()\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n const owner = inspectorPortOwner()\n if (owner != null && owner !== pid) {\n deps.stderr(`inspector 端口被 pid=${owner} 占用。\\n${nativeFallbackHint(pid)}\\n`)\n return 1\n }\n\n const session = new CdpSession()\n try {\n const target = await openInspector(pid)\n verifyTargetIdentity(target, pid)\n await session.connect(target.webSocketDebuggerUrl)\n\n deps.stdout(`采样中(${windowSec}s 窗口,pid=${pid})……\\n`)\n await session.send('HeapProfiler.enable')\n await session.send('HeapProfiler.startSampling', { samplingInterval: 16384 })\n await new Promise((resolve) => setTimeout(resolve, windowSec * 1000))\n const { profile } = await session.send<{ profile: { head: SamplingNode } }>('HeapProfiler.stopSampling')\n\n // 聚合:同一 functionName@url 的 selfSize 求和(top 帧视角,不展开调用树)\n const frames = new Map<string, number>()\n const walk = (node: SamplingNode): void => {\n if (node.selfSize > 0) {\n const fn = node.callFrame.functionName || '(anonymous)'\n const url = node.callFrame.url.replace(/^file:\\/\\/.*\\/(packages|node_modules)\\//, '$1/')\n const key = `${fn} @ ${url || '(native)'}`\n frames.set(key, (frames.get(key) ?? 0) + node.selfSize)\n }\n for (const child of node.children ?? []) walk(child)\n }\n walk(profile.head)\n\n const top = [...frames.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)\n const total = [...frames.values()].reduce((a, b) => a + b, 0)\n deps.stdout(`\\n— Top 分配帧(采样窗口内共 ${(total / 1048576).toFixed(1)}MB 采样分配)—\\n`)\n deps.stdout('SIZE(MB) % FRAME\\n')\n for (const [frame, size] of top) {\n const mb = (size / 1048576).toFixed(2)\n const pct = total > 0 ? ((size / total) * 100).toFixed(1) : '0.0'\n deps.stdout(`${mb.padEnd(9)} ${(pct + '%').padEnd(6)} ${frame}\\n`)\n }\n deps.stdout(\n '\\n判读提示(lesson_92):top 帧若是 measure @ node:internal/perf/performance → user-timing 无界缓冲\\n' +\n '(React dev 构建特征,检查 NODE_ENV);若是业务代码帧 → 进入 otto doctor attach 看 active handles。\\n',\n )\n return 0\n } catch (error) {\n deps.stderr(`otto doctor heap 失败: ${error instanceof Error ? error.message : String(error)}\\n`)\n deps.stdout('\\n降级配方:\\n' + nativeFallbackHint(pid) + '\\n')\n return 1\n } finally {\n session.close()\n }\n}\n"],"mappings":";2EAyBA,eAAsB,EAAc,EAAgB,EAAmC,CACrF,IAAM,EAAa,EAAK,OAAQ,GAAM,CAAC,EAAE,WAAW,IAAI,CAAC,CACnD,EAAM,OAAO,EAAW,GAAG,CACjC,GAAI,CAAC,OAAO,SAAS,EAAI,EAAI,GAAO,EAElC,OADA,EAAK,OAAO;EAAoD,CACzD,EAGT,IAAM,EAAY,EAAK,QAAQ,WAAW,CACpC,EAAY,IAAc,GAAyC,GAApC,OAAO,EAAK,EAAY,GAAG,EAAI,GAEpE,GAAI,CACF,GAAwB,OACjB,EAAO,CAEd,OADA,EAAK,OAAO,GAAG,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CACnE,EAGT,IAAM,EAAQ,GAAoB,CAClC,GAAI,GAAS,MAAQ,IAAU,EAE7B,OADA,EAAK,OAAO,qBAAqB,EAAM,QAAQ,EAAmB,EAAI,CAAC,IAAI,CACpE,EAGT,IAAM,EAAU,IAAI,EACpB,GAAI,CACF,IAAM,EAAS,MAAM,EAAc,EAAI,CACvC,EAAqB,EAAQ,EAAI,CACjC,MAAM,EAAQ,QAAQ,EAAO,qBAAqB,CAElD,EAAK,OAAO,OAAO,EAAU,WAAW,EAAI,OAAO,CACnD,MAAM,EAAQ,KAAK,sBAAsB,CACzC,MAAM,EAAQ,KAAK,6BAA8B,CAAE,iBAAkB,MAAO,CAAC,CAC7E,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAY,IAAK,CAAC,CACrE,GAAM,CAAE,WAAY,MAAM,EAAQ,KAA0C,4BAA4B,CAGlG,EAAS,IAAI,IACb,EAAQ,GAA6B,CACzC,GAAI,EAAK,SAAW,EAAG,CAGrB,IAAM,EAAM,GAFD,EAAK,UAAU,cAAgB,cAExB,KADN,EAAK,UAAU,IAAI,QAAQ,0CAA2C,MAAM,EAC1D,aAC9B,EAAO,IAAI,GAAM,EAAO,IAAI,EAAI,EAAI,GAAK,EAAK,SAAS,CAEzD,IAAK,IAAM,KAAS,EAAK,UAAY,EAAE,CAAE,EAAK,EAAM,EAEtD,EAAK,EAAQ,KAAK,CAElB,IAAM,EAAM,CAAC,GAAG,EAAO,SAAS,CAAC,CAAC,MAAM,EAAG,IAAM,EAAE,GAAK,EAAE,GAAG,CAAC,MAAM,EAAG,GAAG,CACpE,EAAQ,CAAC,GAAG,EAAO,QAAQ,CAAC,CAAC,QAAQ,EAAG,IAAM,EAAI,EAAG,EAAE,CAC7D,EAAK,OAAO,uBAAuB,EAAQ,SAAS,QAAQ,EAAE,CAAC,aAAa,CAC5E,EAAK,OAAO;EAA2B,CACvC,IAAK,GAAM,CAAC,EAAO,KAAS,EAAK,CAC/B,IAAM,GAAM,EAAO,SAAS,QAAQ,EAAE,CAChC,EAAM,EAAQ,GAAM,EAAO,EAAS,KAAK,QAAQ,EAAE,CAAG,MAC5D,EAAK,OAAO,GAAG,EAAG,OAAO,EAAE,CAAC,IAAI,EAAM,KAAK,OAAO,EAAE,CAAC,GAAG,EAAM,IAAI,CAMpE,OAJA,EAAK,OACH;;;EAED,CACM,QACA,EAAO,CAGd,OAFA,EAAK,OAAO,wBAAwB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CAC/F,EAAK,OAAO;;EAAc,EAAmB,EAAI,CAAG;EAAK,CAClD,SACC,CACR,EAAQ,OAAO"}
1
+ {"version":3,"file":"doctor-heap-Eu3LZj-F.js","names":[],"sources":["../src/commands/handlers/doctor-heap.ts"],"sourcesContent":["/**\n * doctor-heap.ts — 短窗 heap 分配采样 + top 分配帧(RFC-200 D1 / T200-3)。\n *\n * lesson_92 方法论固化:「heap sampling 的 top 分配帧直接指向驻留源,比逐结构猜测快得多」\n * ——2GB user-timing 驻留正是靠 top 帧 `measure @ node:internal/perf/performance` 一击定位。\n * 流程:安全附加(复用 doctor-cdp 三步)→ HeapProfiler.startSampling → 等待采样窗口 →\n * stopSampling → 聚合调用树 → 按 selfSize 排序输出 top 帧。只读,不触发 GC。\n */\nimport {\n CdpSession,\n assertBuiltinWebSocket,\n inspectorPortOwner,\n nativeFallbackHint,\n openInspector,\n verifyTargetIdentity,\n} from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\ninterface SamplingNode {\n callFrame: { functionName: string; url: string }\n selfSize: number\n children?: SamplingNode[]\n}\n\nexport async function runDoctorHeap(args: string[], deps: DoctorDeps): Promise<number> {\n const positional = args.filter((a) => !a.startsWith('-'))\n const pid = Number(positional[0])\n if (!Number.isFinite(pid) || pid <= 0) {\n deps.stderr('用法: otto doctor heap <pid> [--window <秒,默认 10>]\\n')\n return 1\n }\n\n const windowIdx = args.indexOf('--window')\n const windowSec = windowIdx !== -1 ? Number(args[windowIdx + 1]) || 10 : 10\n\n try {\n assertBuiltinWebSocket()\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n const owner = inspectorPortOwner()\n if (owner != null && owner !== pid) {\n deps.stderr(`inspector 端口被 pid=${owner} 占用。\\n${nativeFallbackHint(pid)}\\n`)\n return 1\n }\n\n const session = new CdpSession()\n try {\n const target = await openInspector(pid)\n verifyTargetIdentity(target, pid)\n await session.connect(target.webSocketDebuggerUrl)\n\n deps.stdout(`采样中(${windowSec}s 窗口,pid=${pid})……\\n`)\n await session.send('HeapProfiler.enable')\n await session.send('HeapProfiler.startSampling', { samplingInterval: 16384 })\n await new Promise((resolve) => setTimeout(resolve, windowSec * 1000))\n const { profile } = await session.send<{ profile: { head: SamplingNode } }>('HeapProfiler.stopSampling')\n\n // 聚合:同一 functionName@url 的 selfSize 求和(top 帧视角,不展开调用树)\n const frames = new Map<string, number>()\n const walk = (node: SamplingNode): void => {\n if (node.selfSize > 0) {\n const fn = node.callFrame.functionName || '(anonymous)'\n const url = node.callFrame.url.replace(/^file:\\/\\/.*\\/(packages|node_modules)\\//, '$1/')\n const key = `${fn} @ ${url || '(native)'}`\n frames.set(key, (frames.get(key) ?? 0) + node.selfSize)\n }\n for (const child of node.children ?? []) walk(child)\n }\n walk(profile.head)\n\n const top = [...frames.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)\n const total = [...frames.values()].reduce((a, b) => a + b, 0)\n deps.stdout(`\\n— Top 分配帧(采样窗口内共 ${(total / 1048576).toFixed(1)}MB 采样分配)—\\n`)\n deps.stdout('SIZE(MB) % FRAME\\n')\n for (const [frame, size] of top) {\n const mb = (size / 1048576).toFixed(2)\n const pct = total > 0 ? ((size / total) * 100).toFixed(1) : '0.0'\n deps.stdout(`${mb.padEnd(9)} ${(pct + '%').padEnd(6)} ${frame}\\n`)\n }\n deps.stdout(\n '\\n判读提示(lesson_92):top 帧若是 measure @ node:internal/perf/performance → user-timing 无界缓冲\\n' +\n '(React dev 构建特征,检查 NODE_ENV);若是业务代码帧 → 进入 otto doctor attach 看 active handles。\\n',\n )\n return 0\n } catch (error) {\n deps.stderr(`otto doctor heap 失败: ${error instanceof Error ? error.message : String(error)}\\n`)\n deps.stdout('\\n降级配方:\\n' + nativeFallbackHint(pid) + '\\n')\n return 1\n } finally {\n session.close()\n }\n}\n"],"mappings":";2EAyBA,eAAsB,EAAc,EAAgB,EAAmC,CACrF,IAAM,EAAa,EAAK,OAAQ,GAAM,CAAC,EAAE,WAAW,IAAI,CAAC,CACnD,EAAM,OAAO,EAAW,GAAG,CACjC,GAAI,CAAC,OAAO,SAAS,EAAI,EAAI,GAAO,EAElC,OADA,EAAK,OAAO;EAAoD,CACzD,EAGT,IAAM,EAAY,EAAK,QAAQ,WAAW,CACpC,EAAY,IAAc,GAAyC,GAApC,OAAO,EAAK,EAAY,GAAG,EAAI,GAEpE,GAAI,CACF,GAAwB,OACjB,EAAO,CAEd,OADA,EAAK,OAAO,GAAG,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CACnE,EAGT,IAAM,EAAQ,GAAoB,CAClC,GAAI,GAAS,MAAQ,IAAU,EAE7B,OADA,EAAK,OAAO,qBAAqB,EAAM,QAAQ,EAAmB,EAAI,CAAC,IAAI,CACpE,EAGT,IAAM,EAAU,IAAI,EACpB,GAAI,CACF,IAAM,EAAS,MAAM,EAAc,EAAI,CACvC,EAAqB,EAAQ,EAAI,CACjC,MAAM,EAAQ,QAAQ,EAAO,qBAAqB,CAElD,EAAK,OAAO,OAAO,EAAU,WAAW,EAAI,OAAO,CACnD,MAAM,EAAQ,KAAK,sBAAsB,CACzC,MAAM,EAAQ,KAAK,6BAA8B,CAAE,iBAAkB,MAAO,CAAC,CAC7E,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAY,IAAK,CAAC,CACrE,GAAM,CAAE,WAAY,MAAM,EAAQ,KAA0C,4BAA4B,CAGlG,EAAS,IAAI,IACb,EAAQ,GAA6B,CACzC,GAAI,EAAK,SAAW,EAAG,CAGrB,IAAM,EAAM,GAFD,EAAK,UAAU,cAAgB,cAExB,KADN,EAAK,UAAU,IAAI,QAAQ,0CAA2C,MAAM,EAC1D,aAC9B,EAAO,IAAI,GAAM,EAAO,IAAI,EAAI,EAAI,GAAK,EAAK,SAAS,CAEzD,IAAK,IAAM,KAAS,EAAK,UAAY,EAAE,CAAE,EAAK,EAAM,EAEtD,EAAK,EAAQ,KAAK,CAElB,IAAM,EAAM,CAAC,GAAG,EAAO,SAAS,CAAC,CAAC,MAAM,EAAG,IAAM,EAAE,GAAK,EAAE,GAAG,CAAC,MAAM,EAAG,GAAG,CACpE,EAAQ,CAAC,GAAG,EAAO,QAAQ,CAAC,CAAC,QAAQ,EAAG,IAAM,EAAI,EAAG,EAAE,CAC7D,EAAK,OAAO,uBAAuB,EAAQ,SAAS,QAAQ,EAAE,CAAC,aAAa,CAC5E,EAAK,OAAO;EAA2B,CACvC,IAAK,GAAM,CAAC,EAAO,KAAS,EAAK,CAC/B,IAAM,GAAM,EAAO,SAAS,QAAQ,EAAE,CAChC,EAAM,EAAQ,GAAM,EAAO,EAAS,KAAK,QAAQ,EAAE,CAAG,MAC5D,EAAK,OAAO,GAAG,EAAG,OAAO,EAAE,CAAC,IAAI,EAAM,KAAK,OAAO,EAAE,CAAC,GAAG,EAAM,IAAI,CAMpE,OAJA,EAAK,OACH;;;EAED,CACM,QACA,EAAO,CAGd,OAFA,EAAK,OAAO,wBAAwB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CAC/F,EAAK,OAAO;;EAAc,EAAmB,EAAI,CAAG;EAAK,CAClD,SACC,CACR,EAAQ,OAAO"}
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{i as e,n as t}from"./doctor-cdp-K_mIFRfA.js";import{listOttoProcesses as n}from"@x-otto/env";async function r(r,i){let a=r.includes(`--json`),o=n(),s=e(),c=o.map(e=>({pid:e.pid,ppid:e.ppid,rssMb:Math.round(e.rssKb/1024),uptimeH:Number((e.etimeMs/36e5).toFixed(1)),sessionId:e.sessionId?e.sessionId.slice(0,8):void 0,generation:e.generation,zombieSuspect:e.ppid===1&&e.etimeMs>216e5,holdsInspectorPort:s===e.pid}));if(a)return i.stdout(JSON.stringify({processes:c,inspectorPort:{port:t,owner:s??null,ownedByForeignProcess:s!=null&&!o.some(e=>e.pid===s)}},null,2)+`
2
+ import{i as e,n as t}from"./doctor-cdp-CnJUVNOB.js";import{listOttoProcesses as n}from"@x-otto/env";async function r(r,i){let a=r.includes(`--json`),o=n(),s=e(),c=o.map(e=>({pid:e.pid,ppid:e.ppid,rssMb:Math.round(e.rssKb/1024),uptimeH:Number((e.etimeMs/36e5).toFixed(1)),sessionId:e.sessionId?e.sessionId.slice(0,8):void 0,generation:e.generation,zombieSuspect:e.ppid===1&&e.etimeMs>216e5,holdsInspectorPort:s===e.pid}));if(a)return i.stdout(JSON.stringify({processes:c,inspectorPort:{port:t,owner:s??null,ownedByForeignProcess:s!=null&&!o.some(e=>e.pid===s)}},null,2)+`
3
3
  `),0;if(c.length===0)i.stdout(`otto doctor ps: 未发现运行中的 otto 主进程
4
4
  `);else{i.stdout(`PID PPID RSS(MB) UP(h) SESSION GEN FLAGS
5
5
  `);for(let e of c){let t=[e.zombieSuspect?`ZOMBIE?`:``,e.holdsInspectorPort?`INSPECTOR`:``].filter(Boolean).join(`,`)||`-`;i.stdout(`${String(e.pid).padEnd(8)} ${String(e.ppid).padEnd(6)} ${String(e.rssMb).padEnd(8)} ${String(e.uptimeH).padEnd(7)} ${(e.sessionId??`-`).padEnd(9)} ${String(e.generation??`-`).padEnd(4)} ${t}\n`)}}if(s===null)i.stdout(`\ninspector 端口 ${t}:无法检测(lsof 不可用)\n`);else if(s===void 0)i.stdout(`\ninspector 端口 ${t}:空闲(可安全 otto doctor attach <pid>)\n`);else{let e=!o.some(e=>e.pid===s);i.stdout(`\ninspector 端口 ${t}:被 pid=${s} 占用`+(e?`(⚠ 非 otto 进程——attach 前须先让占用者释放,lesson_69)
6
6
  `:`(otto 进程)
7
7
  `))}return 0}export{r as runDoctorPs};
8
- //# sourceMappingURL=doctor-ps-OL3FGQYu.js.map
8
+ //# sourceMappingURL=doctor-ps-JBgCH77n.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-ps-OL3FGQYu.js","names":[],"sources":["../src/commands/handlers/doctor-ps.ts"],"sourcesContent":["/**\n * /doctor ps 命令:进程舰队巡检(Tree/RSS/重启代际)。\n */\nimport { listOttoProcesses } from '@x-otto/env'\n\nimport { INSPECTOR_PORT, inspectorPortOwner } from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\n/** 僵尸嫌疑阈值:ppid=1(孤儿)且存活超 6 小时(lesson_85 的 19h 僵尸群特征收窄)。 */\nconst ZOMBIE_ETIME_MS = 6 * 3600 * 1000\n\nexport async function runDoctorPs(args: string[], deps: DoctorDeps): Promise<number> {\n const json = args.includes('--json')\n const procs = listOttoProcesses()\n const portOwner = inspectorPortOwner()\n\n const rows = procs.map((p) => ({\n pid: p.pid,\n ppid: p.ppid,\n rssMb: Math.round(p.rssKb / 1024),\n uptimeH: Number((p.etimeMs / 3600_000).toFixed(1)),\n sessionId: p.sessionId ? p.sessionId.slice(0, 8) : undefined,\n generation: p.generation,\n /** 诊断字段:孤儿 + 长存活 = 僵尸嫌疑(建议人工核实后处理,本命令不动手)。 */\n zombieSuspect: p.ppid === 1 && p.etimeMs > ZOMBIE_ETIME_MS,\n /** 诊断字段:该进程是否持有 9229 inspector 端口。 */\n holdsInspectorPort: portOwner === p.pid,\n }))\n\n if (json) {\n deps.stdout(JSON.stringify({\n processes: rows,\n inspectorPort: {\n port: INSPECTOR_PORT,\n owner: portOwner ?? null,\n // 端口被非 otto 进程占用(如 Electron --inspect)是 attach 的头号陷阱(lesson_69)\n ownedByForeignProcess: portOwner != null && !procs.some((p) => p.pid === portOwner),\n },\n }, null, 2) + '\\n')\n return 0\n }\n\n if (rows.length === 0) {\n deps.stdout('otto doctor ps: 未发现运行中的 otto 主进程\\n')\n } else {\n deps.stdout('PID PPID RSS(MB) UP(h) SESSION GEN FLAGS\\n')\n for (const r of rows) {\n const flags = [\n r.zombieSuspect ? 'ZOMBIE?' : '',\n r.holdsInspectorPort ? 'INSPECTOR' : '',\n ].filter(Boolean).join(',') || '-'\n deps.stdout(\n `${String(r.pid).padEnd(8)} ${String(r.ppid).padEnd(6)} ${String(r.rssMb).padEnd(8)} ` +\n `${String(r.uptimeH).padEnd(7)} ${(r.sessionId ?? '-').padEnd(9)} ${String(r.generation ?? '-').padEnd(4)} ${flags}\\n`,\n )\n }\n }\n\n if (portOwner === null) {\n deps.stdout(`\\ninspector 端口 ${INSPECTOR_PORT}:无法检测(lsof 不可用)\\n`)\n } else if (portOwner === undefined) {\n deps.stdout(`\\ninspector 端口 ${INSPECTOR_PORT}:空闲(可安全 otto doctor attach <pid>)\\n`)\n } else {\n const foreign = !procs.some((p) => p.pid === portOwner)\n deps.stdout(\n `\\ninspector 端口 ${INSPECTOR_PORT}:被 pid=${portOwner} 占用` +\n (foreign ? '(⚠ 非 otto 进程——attach 前须先让占用者释放,lesson_69)\\n' : '(otto 进程)\\n'),\n )\n }\n return 0\n}\n"],"mappings":";oGAYA,eAAsB,EAAY,EAAgB,EAAmC,CACnF,IAAM,EAAO,EAAK,SAAS,SAAS,CAC9B,EAAQ,GAAmB,CAC3B,EAAY,GAAoB,CAEhC,EAAO,EAAM,IAAK,IAAO,CAC7B,IAAK,EAAE,IACP,KAAM,EAAE,KACR,MAAO,KAAK,MAAM,EAAE,MAAQ,KAAK,CACjC,QAAS,QAAQ,EAAE,QAAU,MAAU,QAAQ,EAAE,CAAC,CAClD,UAAW,EAAE,UAAY,EAAE,UAAU,MAAM,EAAG,EAAE,CAAG,IAAA,GACnD,WAAY,EAAE,WAEd,cAAe,EAAE,OAAS,GAAK,EAAE,QAAU,MAE3C,mBAAoB,IAAc,EAAE,IACrC,EAAE,CAEH,GAAI,EAUF,OATA,EAAK,OAAO,KAAK,UAAU,CACzB,UAAW,EACX,cAAe,CACb,KAAM,EACN,MAAO,GAAa,KAEpB,sBAAuB,GAAa,MAAQ,CAAC,EAAM,KAAM,GAAM,EAAE,MAAQ,EAAU,CACpF,CACF,CAAE,KAAM,EAAE,CAAG;EAAK,CACZ,EAGT,GAAI,EAAK,SAAW,EAClB,EAAK,OAAO;EAAqC,KAC5C,CACL,EAAK,OAAO;EAA0D,CACtE,IAAK,IAAM,KAAK,EAAM,CACpB,IAAM,EAAQ,CACZ,EAAE,cAAgB,UAAY,GAC9B,EAAE,mBAAqB,YAAc,GACtC,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,EAAI,IAC/B,EAAK,OACH,GAAG,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,GACjF,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAa,KAAK,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,YAAc,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAM,IACpH,EAIL,GAAI,IAAc,KAChB,EAAK,OAAO,kBAAkB,EAAe,mBAAmB,SACvD,IAAc,IAAA,GACvB,EAAK,OAAO,kBAAkB,EAAe,qCAAqC,KAC7E,CACL,IAAM,EAAU,CAAC,EAAM,KAAM,GAAM,EAAE,MAAQ,EAAU,CACvD,EAAK,OACH,kBAAkB,EAAe,SAAS,EAAU,MACnD,EAAU;EAAgD;GAC5D,CAEH,MAAO"}
1
+ {"version":3,"file":"doctor-ps-JBgCH77n.js","names":[],"sources":["../src/commands/handlers/doctor-ps.ts"],"sourcesContent":["/**\n * /doctor ps 命令:进程舰队巡检(Tree/RSS/重启代际)。\n */\nimport { listOttoProcesses } from '@x-otto/env'\n\nimport { INSPECTOR_PORT, inspectorPortOwner } from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\n/** 僵尸嫌疑阈值:ppid=1(孤儿)且存活超 6 小时(lesson_85 的 19h 僵尸群特征收窄)。 */\nconst ZOMBIE_ETIME_MS = 6 * 3600 * 1000\n\nexport async function runDoctorPs(args: string[], deps: DoctorDeps): Promise<number> {\n const json = args.includes('--json')\n const procs = listOttoProcesses()\n const portOwner = inspectorPortOwner()\n\n const rows = procs.map((p) => ({\n pid: p.pid,\n ppid: p.ppid,\n rssMb: Math.round(p.rssKb / 1024),\n uptimeH: Number((p.etimeMs / 3600_000).toFixed(1)),\n sessionId: p.sessionId ? p.sessionId.slice(0, 8) : undefined,\n generation: p.generation,\n /** 诊断字段:孤儿 + 长存活 = 僵尸嫌疑(建议人工核实后处理,本命令不动手)。 */\n zombieSuspect: p.ppid === 1 && p.etimeMs > ZOMBIE_ETIME_MS,\n /** 诊断字段:该进程是否持有 9229 inspector 端口。 */\n holdsInspectorPort: portOwner === p.pid,\n }))\n\n if (json) {\n deps.stdout(JSON.stringify({\n processes: rows,\n inspectorPort: {\n port: INSPECTOR_PORT,\n owner: portOwner ?? null,\n // 端口被非 otto 进程占用(如 Electron --inspect)是 attach 的头号陷阱(lesson_69)\n ownedByForeignProcess: portOwner != null && !procs.some((p) => p.pid === portOwner),\n },\n }, null, 2) + '\\n')\n return 0\n }\n\n if (rows.length === 0) {\n deps.stdout('otto doctor ps: 未发现运行中的 otto 主进程\\n')\n } else {\n deps.stdout('PID PPID RSS(MB) UP(h) SESSION GEN FLAGS\\n')\n for (const r of rows) {\n const flags = [\n r.zombieSuspect ? 'ZOMBIE?' : '',\n r.holdsInspectorPort ? 'INSPECTOR' : '',\n ].filter(Boolean).join(',') || '-'\n deps.stdout(\n `${String(r.pid).padEnd(8)} ${String(r.ppid).padEnd(6)} ${String(r.rssMb).padEnd(8)} ` +\n `${String(r.uptimeH).padEnd(7)} ${(r.sessionId ?? '-').padEnd(9)} ${String(r.generation ?? '-').padEnd(4)} ${flags}\\n`,\n )\n }\n }\n\n if (portOwner === null) {\n deps.stdout(`\\ninspector 端口 ${INSPECTOR_PORT}:无法检测(lsof 不可用)\\n`)\n } else if (portOwner === undefined) {\n deps.stdout(`\\ninspector 端口 ${INSPECTOR_PORT}:空闲(可安全 otto doctor attach <pid>)\\n`)\n } else {\n const foreign = !procs.some((p) => p.pid === portOwner)\n deps.stdout(\n `\\ninspector 端口 ${INSPECTOR_PORT}:被 pid=${portOwner} 占用` +\n (foreign ? '(⚠ 非 otto 进程——attach 前须先让占用者释放,lesson_69)\\n' : '(otto 进程)\\n'),\n )\n }\n return 0\n}\n"],"mappings":";oGAYA,eAAsB,EAAY,EAAgB,EAAmC,CACnF,IAAM,EAAO,EAAK,SAAS,SAAS,CAC9B,EAAQ,GAAmB,CAC3B,EAAY,GAAoB,CAEhC,EAAO,EAAM,IAAK,IAAO,CAC7B,IAAK,EAAE,IACP,KAAM,EAAE,KACR,MAAO,KAAK,MAAM,EAAE,MAAQ,KAAK,CACjC,QAAS,QAAQ,EAAE,QAAU,MAAU,QAAQ,EAAE,CAAC,CAClD,UAAW,EAAE,UAAY,EAAE,UAAU,MAAM,EAAG,EAAE,CAAG,IAAA,GACnD,WAAY,EAAE,WAEd,cAAe,EAAE,OAAS,GAAK,EAAE,QAAU,MAE3C,mBAAoB,IAAc,EAAE,IACrC,EAAE,CAEH,GAAI,EAUF,OATA,EAAK,OAAO,KAAK,UAAU,CACzB,UAAW,EACX,cAAe,CACb,KAAM,EACN,MAAO,GAAa,KAEpB,sBAAuB,GAAa,MAAQ,CAAC,EAAM,KAAM,GAAM,EAAE,MAAQ,EAAU,CACpF,CACF,CAAE,KAAM,EAAE,CAAG;EAAK,CACZ,EAGT,GAAI,EAAK,SAAW,EAClB,EAAK,OAAO;EAAqC,KAC5C,CACL,EAAK,OAAO;EAA0D,CACtE,IAAK,IAAM,KAAK,EAAM,CACpB,IAAM,EAAQ,CACZ,EAAE,cAAgB,UAAY,GAC9B,EAAE,mBAAqB,YAAc,GACtC,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,EAAI,IAC/B,EAAK,OACH,GAAG,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,GACjF,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAa,KAAK,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,YAAc,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAM,IACpH,EAIL,GAAI,IAAc,KAChB,EAAK,OAAO,kBAAkB,EAAe,mBAAmB,SACvD,IAAc,IAAA,GACvB,EAAK,OAAO,kBAAkB,EAAe,qCAAqC,KAC7E,CACL,IAAM,EAAU,CAAC,EAAM,KAAM,GAAM,EAAE,MAAQ,EAAU,CACvD,EAAK,OACH,kBAAkB,EAAe,SAAS,EAAU,MACnD,EAAU;EAAgD;GAC5D,CAEH,MAAO"}
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import{exportTrajectoryFromSession as e}from"@x-otto/coding";import{OTTO_HOME as t,OTTO_SESSIONS_DIR as n}from"@x-otto/env";import{dirname as r,join as i}from"node:path";import{createWriteStream as a}from"node:fs";import{mkdir as o,readFile as s}from"node:fs/promises";import{SqliteSessionRepository as c}from"@x-otto/session";const l=`用法:
3
+ otto export-trajectory --session <id> --out <path> [--since-seq <n>]
4
+
5
+ 选项:
6
+ --session <id> 会话 ID(必填;支持短 ID 前缀匹配)
7
+ --out <path> 导出目标路径(JSONL;必填——显式授权)
8
+ --since-seq <n> 增量游标:仅导出 seq > n 的条目(缺省 = 全量)
9
+ --signals-dir <d> 信号目录(RFC-388 冷进程读盘;缺省 = OTTO_HOME/signals)
10
+ `;function u(e,t){let n=e.indexOf(t);if(n<0)return;let r=e[n+1];if(!(r===void 0||r.startsWith(`--`)))return r}async function d(e,t){let n=i(e,`signals.jsonl`),r;try{r=await s(n,`utf-8`)}catch{return[]}let a=[];for(let e of r.split(`
11
+ `)){let n=e.trim();if(n)try{a.push(JSON.parse(n))}catch{t(`otto export-trajectory: 跳过无法解析的信号行(${n.slice(0,60)}…)\n`)}}return a}async function f(e,t,n){if(await e.getSession(t))return t;let r=(await e.listAllSessionIds()).filter(e=>e.startsWith(t));return r.length===1?r[0]:r.length>1?(n(`otto export-trajectory: 会话前缀 "${t}" 匹配 ${r.length} 个会话,请给更完整的 ID。\n`),null):(n(`otto export-trajectory: 找不到 ID 前缀为 "${t}" 的会话。\n`),null)}async function p(s,p={}){let m=p.stdout??(e=>process.stdout.write(e)),h=p.stderr??(e=>process.stderr.write(e));if(s.includes(`--help`)||s.includes(`-h`))return m(l),0;let g=u(s,`--session`),_=u(s,`--out`),v=u(s,`--since-seq`),y=u(s,`--signals-dir`)??p.signalsDir??`${t}/signals`;if(!g||!_)return h(`otto export-trajectory: 需显式授权导出——请同时提供 --session 与 --out。
12
+
13
+ `),h(l),1;let b;if(v!==void 0&&(b=Number(v),!Number.isInteger(b)||b<0))return h(`otto export-trajectory: --since-seq 必须是非负整数,收到 "${v}"。\n`),1;let x=new c({dbPath:i(p.sessionsDir??n,`sessions.db`)});try{let t=await f(x,g,h);if(!t)return 1;await o(r(_),{recursive:!0});let n=a(_,{flags:`w`,encoding:`utf-8`}),i=new Promise((e,t)=>{n.on(`finish`,e),n.on(`error`,t)}),s;try{let r=await d(y,h);s=await e({sessionId:t,repo:x,sinceSeq:b,signals:r,output:n}),n.end(),await i}catch(e){throw n.destroy(),e}return m(`已导出会话 ${t.slice(0,8)}:${s.entryCount} entries → ${s.written} 行${s.suppressed>0?`(脱敏抑制 ${s.suppressed} 行)`:``}(最后导出 seq ${s.upperSeq-1})→ ${_}\n`),s.entryCount>0&&m(`增量续传:otto export-trajectory --session ${t.slice(0,8)} --out <新路径> --since-seq ${s.upperSeq-1}\n`),0}catch(e){return h(`otto export-trajectory: ${e instanceof Error?e.message:String(e)}\n`),1}finally{x.close()}}export{p as runExportTrajectoryCommand};
14
+ //# sourceMappingURL=export-trajectory-D-BcH0Wt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-trajectory-D-BcH0Wt.js","names":[],"sources":["../src/commands/export-trajectory.ts"],"sourcesContent":["/**\n * `otto export-trajectory` 子命令 —— RFC-385 M2:第④层数据出口(on-demand 轨迹导出)。\n *\n * 读 sessions.db 的 session_entries(append-only canonical log)→ captureTrajectory →\n * redaction waterfall(默认无规则)→ JSONL 流式写。默认关:无 --out 不导出(显式授权)。\n *\n * 数据源 = 本地 SQLite(OTTO_SESSIONS_DIR/sessions.db)+ 可选信号文件(RFC-388 M1\n * signals.jsonl,--signals-dir 显式授权读盘,缺省尝试 OTTO_HOME/signals)。信号\n * 读盘 fail-soft:文件缺失/解析失败 warn 后按空信号继续(观测数据,与 store\n * restore 同哲学)。\n */\nimport { createWriteStream } from 'node:fs'\nimport { mkdir, readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\nimport { OTTO_HOME, OTTO_SESSIONS_DIR } from '@x-otto/env'\nimport { exportTrajectoryFromSession } from '@x-otto/coding'\nimport type { EvolutionSignalRecord } from '@x-otto/memory'\nimport { SqliteSessionRepository } from '@x-otto/session'\n\nexport interface ExportTrajectoryDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n /** sessions 目录(测试注入;缺省 = OTTO_SESSIONS_DIR)。 */\n sessionsDir?: string\n /** 信号目录(测试注入;缺省 = OTTO_HOME/signals)。 */\n signalsDir?: string\n}\n\nconst USAGE = `用法:\n otto export-trajectory --session <id> --out <path> [--since-seq <n>]\n\n选项:\n --session <id> 会话 ID(必填;支持短 ID 前缀匹配)\n --out <path> 导出目标路径(JSONL;必填——显式授权)\n --since-seq <n> 增量游标:仅导出 seq > n 的条目(缺省 = 全量)\n --signals-dir <d> 信号目录(RFC-388 冷进程读盘;缺省 = OTTO_HOME/signals)\n`\n\nfunction readFlag(args: readonly string[], flag: string): string | undefined {\n const i = args.indexOf(flag)\n if (i < 0) return undefined\n const value = args[i + 1]\n if (value === undefined || value.startsWith('--')) return undefined\n return value\n}\n\n/**\n * 冷进程信号读盘(RFC-388 后续增强接线):读 signals.jsonl → EvolutionSignalRecord[]。\n * fail-soft:文件缺失(首启无信号)/单行解析失败均 warn 后返回空数组——信号是\n * 观测数据,缺信号不阻断导出(与 store restoreFromLog 同哲学)。\n */\nasync function loadSignalsFromDisk(\n signalsDir: string,\n warn: (s: string) => void,\n): Promise<EvolutionSignalRecord[]> {\n const path = join(signalsDir, 'signals.jsonl')\n let raw: string\n try {\n raw = await readFile(path, 'utf-8')\n } catch {\n return [] // 首启无信号文件——合法空态,静默。\n }\n const records: EvolutionSignalRecord[] = []\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed) continue\n try {\n records.push(JSON.parse(trimmed) as EvolutionSignalRecord)\n } catch {\n warn(`otto export-trajectory: 跳过无法解析的信号行(${trimmed.slice(0, 60)}…)\\n`)\n }\n }\n return records\n}\n\nasync function resolveSessionId(\n repo: SqliteSessionRepository,\n arg: string,\n stderr: (s: string) => void,\n): Promise<string | null> {\n if (await repo.getSession(arg)) return arg\n const all = await repo.listAllSessionIds()\n const matches = all.filter((id) => id.startsWith(arg))\n if (matches.length === 1) return matches[0]!\n if (matches.length > 1) {\n stderr(`otto export-trajectory: 会话前缀 \"${arg}\" 匹配 ${matches.length} 个会话,请给更完整的 ID。\\n`)\n return null\n }\n stderr(`otto export-trajectory: 找不到 ID 前缀为 \"${arg}\" 的会话。\\n`)\n return null\n}\n\nexport async function runExportTrajectoryCommand(\n args: string[],\n deps: ExportTrajectoryDeps = {},\n): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n\n if (args.includes('--help') || args.includes('-h')) {\n stdout(USAGE)\n return 0\n }\n\n const sessionArg = readFlag(args, '--session')\n const outPath = readFlag(args, '--out')\n const sinceSeqRaw = readFlag(args, '--since-seq')\n const signalsDir = readFlag(args, '--signals-dir') ?? deps.signalsDir ?? `${OTTO_HOME}/signals`\n\n if (!sessionArg || !outPath) {\n stderr('otto export-trajectory: 需显式授权导出——请同时提供 --session 与 --out。\\n\\n')\n stderr(USAGE)\n return 1\n }\n\n let sinceSeq: number | undefined\n if (sinceSeqRaw !== undefined) {\n sinceSeq = Number(sinceSeqRaw)\n if (!Number.isInteger(sinceSeq) || sinceSeq < 0) {\n stderr(`otto export-trajectory: --since-seq 必须是非负整数,收到 \"${sinceSeqRaw}\"。\\n`)\n return 1\n }\n }\n\n const sessionsDir = deps.sessionsDir ?? OTTO_SESSIONS_DIR\n const dbPath = join(sessionsDir, 'sessions.db')\n const repo = new SqliteSessionRepository({ dbPath })\n\n try {\n const sessionId = await resolveSessionId(repo, sessionArg, stderr)\n if (!sessionId) return 1\n\n await mkdir(dirname(outPath), { recursive: true })\n const stream = createWriteStream(outPath, { flags: 'w', encoding: 'utf-8' })\n const finished = new Promise<void>((resolve, reject) => {\n stream.on('finish', resolve)\n stream.on('error', reject)\n })\n\n let result: Awaited<ReturnType<typeof exportTrajectoryFromSession>>\n try {\n const signals = await loadSignalsFromDisk(signalsDir, stderr)\n result = await exportTrajectoryFromSession({\n sessionId,\n repo,\n sinceSeq,\n signals,\n output: stream,\n })\n stream.end()\n await finished\n } catch (error) {\n // 出错时销毁半开流(未 end 的流不触发 finish),再向上汇报;repo.close 在 finally。\n stream.destroy()\n throw error\n }\n\n stdout(\n `已导出会话 ${sessionId.slice(0, 8)}:${result.entryCount} entries → ${result.written} 行` +\n `${result.suppressed > 0 ? `(脱敏抑制 ${result.suppressed} 行)` : ''}` +\n `(最后导出 seq ${result.upperSeq - 1})→ ${outPath}\\n`,\n )\n // 续传游标提示(应用点 B 建议修订①):--since-seq 语义是严格大于,续传必须传\n // upperSeq-1;只报 upperSeq 会让用户按字面传 N 静默漏一条。\n if (result.entryCount > 0) {\n stdout(`增量续传:otto export-trajectory --session ${sessionId.slice(0, 8)} --out <新路径> --since-seq ${result.upperSeq - 1}\\n`)\n }\n return 0\n } catch (error) {\n stderr(`otto export-trajectory: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n } finally {\n repo.close()\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{n as e,t}from"./build-id-ClyMIKLy.js";import{t as n}from"./create-host-app-BXQ8bU-B.js";import{platform as r,release as i}from"node:os";import{execFile as a}from"node:child_process";import{openBrowser as o}from"@x-otto/shared";import{SDK_VERSION as s}from"@x-otto/env";import{promisify as c}from"node:util";const l=c(a);function u(e){let t=e.trim().replace(/\.git$/,``),n=t.match(/^git@[^:]+:(.+)$/);if(n)return n[1];let r=t.match(/^https?:\/\/[^/]+\/(.+)$/);if(r)return r[1]}async function d(e){try{let{stdout:t}=await l(`git`,[`remote`,`get-url`,`origin`],{cwd:e,timeout:5e3});return u(t)}catch{return}}async function f(e){return e.configuredRepo?e.configuredRepo:await d(e.cwd)||e.cliRepoArg}function p(n){return[`- otto version: ${s} (build ${e}, ${t}@dev)`,`- Node: ${process.version}`,`- OS: ${r()} ${process.arch} ${i()}`,`- Model: ${n??`(none configured)`}`].join(`
2
+ import{n as e,t}from"./build-id-D_JURZdd.js";import{t as n}from"./create-host-app-BXQ8bU-B.js";import{platform as r,release as i}from"node:os";import{execFile as a}from"node:child_process";import{openBrowser as o}from"@x-otto/shared";import{SDK_VERSION as s}from"@x-otto/env";import{promisify as c}from"node:util";const l=c(a);function u(e){let t=e.trim().replace(/\.git$/,``),n=t.match(/^git@[^:]+:(.+)$/);if(n)return n[1];let r=t.match(/^https?:\/\/[^/]+\/(.+)$/);if(r)return r[1]}async function d(e){try{let{stdout:t}=await l(`git`,[`remote`,`get-url`,`origin`],{cwd:e,timeout:5e3});return u(t)}catch{return}}async function f(e){return e.configuredRepo?e.configuredRepo:await d(e.cwd)||e.cliRepoArg}function p(n){return[`- otto version: ${s} (build ${e}, ${t}@dev)`,`- Node: ${process.version}`,`- OS: ${r()} ${process.arch} ${i()}`,`- Model: ${n??`(none configured)`}`].join(`
3
3
  `)}function m(e){let t,n,r,i,a,o=!1,s=(t,n)=>{let r=e[t+1];if(r===void 0||r.startsWith(`--`))throw Error(`${n} requires a value`);return r};for(let c=0;c<e.length;c++){let l=e[c];l===`--title`?(t=s(c,`--title`),c++):l===`--reproduce`?(n=s(c,`--reproduce`),c++):l===`--expected`?(r=s(c,`--expected`),c++):l===`--actual`?(i=s(c,`--actual`),c++):l===`--repo`?(a=s(c,`--repo`),c++):l===`--browser`&&(o=!0)}return{title:t,reproduce:n,expected:r,actual:i,repo:a,browser:o}}function h(e){e(`Usage: otto feedback --title <title> [options]
4
4
 
5
5
  Report an issue or send feedback via GitHub (non-interactive — for scripting/CI).
@@ -18,4 +18,4 @@ Options:
18
18
  `),1;let n=await f({configuredRepo:s.settings.get(`feedback_repo`),cliRepoArg:a.repo,cwd:t.projectDir??process.cwd()});if(!n)return i(`otto feedback: could not resolve a target repository — pass --repo or set "feedback_repo" in .otto/config.json
19
19
  `),1;let c=p(void 0),l={title:a.title,reproduce:a.reproduce,expected:a.expected,actual:a.actual,envInfo:c,repo:n};if(!a.browser&&e.submitIssueViaGh){let t=await e.submitIssueViaGh(l);if(t.ok)return r(`Issue created: ${t.url}\n`),0;i(`otto feedback: gh channel failed (${t.kind}) — falling back to browser\n`)}let u=e.buildIssueUrl?.(l);return u?(o(u),r(`Opened: ${u}\n`),0):(i(`otto feedback: feedback plugin does not support buildIssueUrl
20
20
  `),1)}finally{await s.stop()}}export{f as n,g as r,p as t};
21
- //# sourceMappingURL=feedback-BQ_oz5nh.js.map
21
+ //# sourceMappingURL=feedback-CYFY2MC4.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"feedback-BQ_oz5nh.js","names":["BUILD_ID"],"sources":["../src/commands/feedback.ts"],"sourcesContent":["/**\n * /feedback 命令的 TUI 接线层(提交/展示)。\n */\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { platform, release } from 'node:os'\nimport { SDK_VERSION } from '@x-otto/env'\nimport { BUILD_ID, BUILD_BRANCH, BUILD_HASH } from '../build-id'\nimport { createHostApp } from '../create-host-app'\nimport type { App } from '@x-otto/coding'\nimport { openBrowser } from '@x-otto/shared'\n\nconst execFileAsync = promisify(execFile)\n\n/** git remote origin URL → `owner/repo`(支持 `git@github.com:owner/repo.git` 与 `https://github.com/owner/repo.git` 两种形态)。 */\nfunction parseOwnerRepoFromGitUrl(url: string): string | undefined {\n const trimmed = url.trim().replace(/\\.git$/, '')\n const sshMatch = trimmed.match(/^git@[^:]+:(.+)$/)\n if (sshMatch) return sshMatch[1]\n const httpsMatch = trimmed.match(/^https?:\\/\\/[^/]+\\/(.+)$/)\n if (httpsMatch) return httpsMatch[1]\n return undefined\n}\n\n/** `git remote get-url origin` 解析当前工作目录的 owner/repo;非 git 仓库/无 origin 时返回 undefined(fail-soft,不抛出)。 */\nasync function resolveGitRemoteRepo(cwd: string): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd, timeout: 5000 })\n return parseOwnerRepoFromGitUrl(stdout)\n } catch {\n return undefined\n }\n}\n\n/**\n * RFC-207 §3.4:目标仓库解析优先级——`feedback_repo` 配置 > git remote 解析 >\n * `--repo` 命令行参数。全部缺失时返回 `undefined`。\n */\nexport async function resolveFeedbackRepo(opts: {\n configuredRepo?: string\n cliRepoArg?: string\n cwd: string\n}): Promise<string | undefined> {\n if (opts.configuredRepo) return opts.configuredRepo\n const fromGit = await resolveGitRemoteRepo(opts.cwd)\n if (fromGit) return fromGit\n return opts.cliRepoArg\n}\n\n/**\n * RFC-207 §3.1 环境信息行(issue body 的\"环境信息\"小节,脱敏——只含版本/运行时/OS/\n * 模型,不含用户路径/环境变量等敏感信息)。\n */\nexport function buildFeedbackEnvInfo(modelId: string | undefined): string {\n return [\n `- otto version: ${SDK_VERSION} (build ${BUILD_ID}, ${BUILD_HASH}@${BUILD_BRANCH})`,\n `- Node: ${process.version}`,\n `- OS: ${platform()} ${process.arch} ${release()}`,\n `- Model: ${modelId ?? '(none configured)'}`,\n ].join('\\n')\n}\n\nexport interface FeedbackCommandDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n projectDir?: string\n configPath?: string\n /** 测试注入:替代生产装配的 app(真实 App 类型——需要 feedbackRegistry/submitIssueViaGh,\n * 非 ServiceHostPort/AppContext 窄接口能覆盖,同 serve.ts 的 AppContext 注入模式不同)。 */\n app?: App\n}\n\nfunction parseFeedbackArgs(args: string[]): {\n title?: string\n reproduce?: string\n expected?: string\n actual?: string\n repo?: string\n browser: boolean\n} {\n let title: string | undefined\n let reproduce: string | undefined\n let expected: string | undefined\n let actual: string | undefined\n let repo: string | undefined\n let browser = false\n\n const takeValue = (i: number, name: string): string => {\n const raw = args[i + 1]\n if (raw === undefined || raw.startsWith('--')) {\n throw new Error(`${name} requires a value`)\n }\n return raw\n }\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (arg === '--title') { title = takeValue(i, '--title'); i++ }\n else if (arg === '--reproduce') { reproduce = takeValue(i, '--reproduce'); i++ }\n else if (arg === '--expected') { expected = takeValue(i, '--expected'); i++ }\n else if (arg === '--actual') { actual = takeValue(i, '--actual'); i++ }\n else if (arg === '--repo') { repo = takeValue(i, '--repo'); i++ }\n else if (arg === '--browser') { browser = true }\n }\n\n return { title, reproduce, expected, actual, repo, browser }\n}\n\nfunction printFeedbackHelp(stdout: (s: string) => void): void {\n stdout(`Usage: otto feedback --title <title> [options]\n\nReport an issue or send feedback via GitHub (non-interactive — for scripting/CI).\nFor an interactive prompt-driven flow, use the \"/feedback\" command inside otto.\n\nOptions:\n --title <title> Short issue title (required)\n --reproduce <text> Steps to reproduce (optional)\n --expected <text> Expected behavior (optional)\n --actual <text> Actual behavior (optional)\n --repo <owner/repo> Target repository (defaults to feedback_repo config, then git remote origin)\n --browser Skip the gh CLI channel and always use the browser fallback\n --help Show this help\n`)\n}\n\n/**\n * `otto feedback` 非交互顶层子命令(RFC-207 §3.2/§3.4)——CI/脚本化调用场景,\n * `--title`/`--body` 系参数直接传参跳过 TUI openInput 链。复用与 `/feedback` 斜杠\n * 命令相同的核心逻辑(`resolveFeedbackRepo`/`buildFeedbackEnvInfo`)+ 同一个\n * `FeedbackRegistry`/插件实现(`app.feedbackRegistry`),两个入口不重复业务逻辑。\n */\nexport async function runFeedbackCommand(args: string[], deps: FeedbackCommandDeps = {}): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n\n if (args[0] === 'help' || args[0] === '--help') {\n printFeedbackHelp(stdout)\n return 0\n }\n\n let parsed: ReturnType<typeof parseFeedbackArgs>\n try {\n parsed = parseFeedbackArgs(args)\n } catch (error) {\n stderr(`otto feedback: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n if (!parsed.title) {\n stderr('otto feedback: --title is required (see --help)\\n')\n return 1\n }\n\n const app =\n deps.app ??\n createHostApp({\n logger: { name: 'otto-feedback', level: 'error', destination: 'stderr' },\n workspaceDir: deps.projectDir,\n projectConfigPath: deps.configPath,\n })\n\n try {\n await app.start()\n } catch (error) {\n stderr(`otto feedback: failed to start app: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n try {\n const provider = app.feedbackRegistry.get()\n if (!provider) {\n stderr('otto feedback: feedback plugin is not available (disabled or not installed)\\n')\n return 1\n }\n\n const configuredRepo = app.settings.get('feedback_repo') as string | undefined\n const repo = await resolveFeedbackRepo({ configuredRepo, cliRepoArg: parsed.repo, cwd: deps.projectDir ?? process.cwd() })\n if (!repo) {\n stderr('otto feedback: could not resolve a target repository — pass --repo or set \"feedback_repo\" in .otto/config.json\\n')\n return 1\n }\n\n const envInfo = buildFeedbackEnvInfo(undefined)\n const input = {\n title: parsed.title,\n reproduce: parsed.reproduce,\n expected: parsed.expected,\n actual: parsed.actual,\n envInfo,\n repo,\n }\n\n if (!parsed.browser && provider.submitIssueViaGh) {\n const result = await provider.submitIssueViaGh(input)\n if (result.ok) {\n stdout(`Issue created: ${result.url}\\n`)\n return 0\n }\n stderr(`otto feedback: gh channel failed (${result.kind}) — falling back to browser\\n`)\n }\n\n const url = provider.buildIssueUrl?.(input)\n if (!url) {\n stderr('otto feedback: feedback plugin does not support buildIssueUrl\\n')\n return 1\n }\n openBrowser(url)\n stdout(`Opened: ${url}\\n`)\n return 0\n } finally {\n await app.stop()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"feedback-CYFY2MC4.js","names":["BUILD_ID"],"sources":["../src/commands/feedback.ts"],"sourcesContent":["/**\n * /feedback 命令的 TUI 接线层(提交/展示)。\n */\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { platform, release } from 'node:os'\nimport { SDK_VERSION } from '@x-otto/env'\nimport { BUILD_ID, BUILD_BRANCH, BUILD_HASH } from '../build-id'\nimport { createHostApp } from '../create-host-app'\nimport type { App } from '@x-otto/coding'\nimport { openBrowser } from '@x-otto/shared'\n\nconst execFileAsync = promisify(execFile)\n\n/** git remote origin URL → `owner/repo`(支持 `git@github.com:owner/repo.git` 与 `https://github.com/owner/repo.git` 两种形态)。 */\nfunction parseOwnerRepoFromGitUrl(url: string): string | undefined {\n const trimmed = url.trim().replace(/\\.git$/, '')\n const sshMatch = trimmed.match(/^git@[^:]+:(.+)$/)\n if (sshMatch) return sshMatch[1]\n const httpsMatch = trimmed.match(/^https?:\\/\\/[^/]+\\/(.+)$/)\n if (httpsMatch) return httpsMatch[1]\n return undefined\n}\n\n/** `git remote get-url origin` 解析当前工作目录的 owner/repo;非 git 仓库/无 origin 时返回 undefined(fail-soft,不抛出)。 */\nasync function resolveGitRemoteRepo(cwd: string): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd, timeout: 5000 })\n return parseOwnerRepoFromGitUrl(stdout)\n } catch {\n return undefined\n }\n}\n\n/**\n * RFC-207 §3.4:目标仓库解析优先级——`feedback_repo` 配置 > git remote 解析 >\n * `--repo` 命令行参数。全部缺失时返回 `undefined`。\n */\nexport async function resolveFeedbackRepo(opts: {\n configuredRepo?: string\n cliRepoArg?: string\n cwd: string\n}): Promise<string | undefined> {\n if (opts.configuredRepo) return opts.configuredRepo\n const fromGit = await resolveGitRemoteRepo(opts.cwd)\n if (fromGit) return fromGit\n return opts.cliRepoArg\n}\n\n/**\n * RFC-207 §3.1 环境信息行(issue body 的\"环境信息\"小节,脱敏——只含版本/运行时/OS/\n * 模型,不含用户路径/环境变量等敏感信息)。\n */\nexport function buildFeedbackEnvInfo(modelId: string | undefined): string {\n return [\n `- otto version: ${SDK_VERSION} (build ${BUILD_ID}, ${BUILD_HASH}@${BUILD_BRANCH})`,\n `- Node: ${process.version}`,\n `- OS: ${platform()} ${process.arch} ${release()}`,\n `- Model: ${modelId ?? '(none configured)'}`,\n ].join('\\n')\n}\n\nexport interface FeedbackCommandDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n projectDir?: string\n configPath?: string\n /** 测试注入:替代生产装配的 app(真实 App 类型——需要 feedbackRegistry/submitIssueViaGh,\n * 非 ServiceHostPort/AppContext 窄接口能覆盖,同 serve.ts 的 AppContext 注入模式不同)。 */\n app?: App\n}\n\nfunction parseFeedbackArgs(args: string[]): {\n title?: string\n reproduce?: string\n expected?: string\n actual?: string\n repo?: string\n browser: boolean\n} {\n let title: string | undefined\n let reproduce: string | undefined\n let expected: string | undefined\n let actual: string | undefined\n let repo: string | undefined\n let browser = false\n\n const takeValue = (i: number, name: string): string => {\n const raw = args[i + 1]\n if (raw === undefined || raw.startsWith('--')) {\n throw new Error(`${name} requires a value`)\n }\n return raw\n }\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (arg === '--title') { title = takeValue(i, '--title'); i++ }\n else if (arg === '--reproduce') { reproduce = takeValue(i, '--reproduce'); i++ }\n else if (arg === '--expected') { expected = takeValue(i, '--expected'); i++ }\n else if (arg === '--actual') { actual = takeValue(i, '--actual'); i++ }\n else if (arg === '--repo') { repo = takeValue(i, '--repo'); i++ }\n else if (arg === '--browser') { browser = true }\n }\n\n return { title, reproduce, expected, actual, repo, browser }\n}\n\nfunction printFeedbackHelp(stdout: (s: string) => void): void {\n stdout(`Usage: otto feedback --title <title> [options]\n\nReport an issue or send feedback via GitHub (non-interactive — for scripting/CI).\nFor an interactive prompt-driven flow, use the \"/feedback\" command inside otto.\n\nOptions:\n --title <title> Short issue title (required)\n --reproduce <text> Steps to reproduce (optional)\n --expected <text> Expected behavior (optional)\n --actual <text> Actual behavior (optional)\n --repo <owner/repo> Target repository (defaults to feedback_repo config, then git remote origin)\n --browser Skip the gh CLI channel and always use the browser fallback\n --help Show this help\n`)\n}\n\n/**\n * `otto feedback` 非交互顶层子命令(RFC-207 §3.2/§3.4)——CI/脚本化调用场景,\n * `--title`/`--body` 系参数直接传参跳过 TUI openInput 链。复用与 `/feedback` 斜杠\n * 命令相同的核心逻辑(`resolveFeedbackRepo`/`buildFeedbackEnvInfo`)+ 同一个\n * `FeedbackRegistry`/插件实现(`app.feedbackRegistry`),两个入口不重复业务逻辑。\n */\nexport async function runFeedbackCommand(args: string[], deps: FeedbackCommandDeps = {}): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n\n if (args[0] === 'help' || args[0] === '--help') {\n printFeedbackHelp(stdout)\n return 0\n }\n\n let parsed: ReturnType<typeof parseFeedbackArgs>\n try {\n parsed = parseFeedbackArgs(args)\n } catch (error) {\n stderr(`otto feedback: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n if (!parsed.title) {\n stderr('otto feedback: --title is required (see --help)\\n')\n return 1\n }\n\n const app =\n deps.app ??\n createHostApp({\n logger: { name: 'otto-feedback', level: 'error', destination: 'stderr' },\n workspaceDir: deps.projectDir,\n projectConfigPath: deps.configPath,\n })\n\n try {\n await app.start()\n } catch (error) {\n stderr(`otto feedback: failed to start app: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n try {\n const provider = app.feedbackRegistry.get()\n if (!provider) {\n stderr('otto feedback: feedback plugin is not available (disabled or not installed)\\n')\n return 1\n }\n\n const configuredRepo = app.settings.get('feedback_repo') as string | undefined\n const repo = await resolveFeedbackRepo({ configuredRepo, cliRepoArg: parsed.repo, cwd: deps.projectDir ?? process.cwd() })\n if (!repo) {\n stderr('otto feedback: could not resolve a target repository — pass --repo or set \"feedback_repo\" in .otto/config.json\\n')\n return 1\n }\n\n const envInfo = buildFeedbackEnvInfo(undefined)\n const input = {\n title: parsed.title,\n reproduce: parsed.reproduce,\n expected: parsed.expected,\n actual: parsed.actual,\n envInfo,\n repo,\n }\n\n if (!parsed.browser && provider.submitIssueViaGh) {\n const result = await provider.submitIssueViaGh(input)\n if (result.ok) {\n stdout(`Issue created: ${result.url}\\n`)\n return 0\n }\n stderr(`otto feedback: gh channel failed (${result.kind}) — falling back to browser\\n`)\n }\n\n const url = provider.buildIssueUrl?.(input)\n if (!url) {\n stderr('otto feedback: feedback plugin does not support buildIssueUrl\\n')\n return 1\n }\n openBrowser(url)\n stdout(`Opened: ${url}\\n`)\n return 0\n } finally {\n await app.stop()\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{r as e}from"./feedback-CYFY2MC4.js";export{e as runFeedbackCommand};
@@ -3,4 +3,4 @@ import{t as e}from"./discover-with-bundled-CF8qFP41.js";import{homedir as t}from
3
3
  `,{mode:384}),e}function d(e){try{return a(e)?JSON.parse(s(e,`utf8`)):void 0}catch{return}}function f(e,t){return new Promise(n=>{e.tui.modal.openModal({kind:`select`,...t,onSelect:e=>n(e),onCancel:()=>n(void 0)})})}function p(e,t,n){return new Promise(r=>{e.tui.modal.openModal({kind:`confirm`,title:t,message:n,onConfirm:()=>r(!0),onCancel:()=>r(!1)})})}function m(e,t=300*1e3,r){return new Promise(o=>{let s=i(e,`scripts`,`register-app.mjs`);if(!a(s)){o(void 0);return}let c=n(`node`,[s],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`],timeout:t,env:{...process.env}}),l=``,u=``,d=!1;c.stdout.on(`data`,e=>{l+=e.toString()}),c.stderr.on(`data`,e=>{if(u+=e.toString(),d||!r)return;let t=u.split(`
4
4
  `).find(e=>e.startsWith(`QRCODE_URL::`));if(t)try{let e=JSON.parse(t.slice(12));e.url&&(d=!0,r(e))}catch{}}),c.on(`error`,()=>o(void 0)),c.on(`close`,e=>{if(e!==0)return o(void 0);try{let e=JSON.parse(l.trim().split(`
5
5
  `).pop());if(e.client_id&&e.client_secret){o(e);return}}catch{}o(void 0)})})}function h(e,t,r){return new Promise(o=>{let s=i(e,`scripts`,`verify-bot.mjs`);if(!a(s)){o(void 0);return}let c=n(`node`,[s],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`],timeout:15e3,env:{...process.env,FEISHU_VERIFY_APP_ID:t,FEISHU_VERIFY_APP_SECRET:r}}),l=``;c.stdout.on(`data`,e=>{l+=e.toString()}),c.on(`error`,()=>o(void 0)),c.on(`close`,e=>{if(e!==0)return o(void 0);try{o(JSON.parse(l.trim()))}catch{o(void 0)}})})}function g(e){return e.length<=12?e.slice(0,4)+`***`:e.slice(0,8)+`***`+e.slice(-4)}async function _(e,t,n,r){if(r?.app_id&&!await p(e,`feishu-use 重配置`,[`当前配置:`,``,...[`App ID: ${g(String(r.app_id))}`,r.allowed_chats?`allowed_chats: ${r.allowed_chats.length} 个`:`allowed_chats: 空(拒绝所有)`,`配置路径: ${t}`],``,`确定要重新配置吗?当前凭据将被覆盖。`]))return;let a=await f(e,{title:`feishu-use 配置`,options:[{label:`扫码自动创建飞书应用(推荐)`,value:`qr`,detail:`飞书 App 扫码确认后自动获取凭据`},{label:`手动输入 App ID / App Secret`,value:`manual`,detail:`已有飞书自建应用,直接填入凭据`}]});if(!a)return;let o=``,s=``,c=[],l=[];if(a===`qr`){e.tui.info.showToast(`plugin-feishu-use`,`正在生成飞书应用注册二维码,请稍候…`);let t=await m(n,300*1e3,t=>{let n=Math.round(t.expireIn/60);e.tui.messages.pushMessage({role:`assistant`,content:[{type:`text`,text:`feishu-use:请在飞书 App 中打开以下链接扫码确认创建应用(${n} 分钟内有效):\n${t.url}`}]})});if(t)o=t.client_id,s=t.client_secret,e.tui.info.showToast(`plugin-feishu-use`,`扫码注册成功!凭据已自动填充。`);else if(!await p(e,`plugin-feishu-use`,`扫码注册失败(超时或网络错误)。是否改为手动输入凭据?`))return}if(!o){let t=r?.app_id?`(当前: ${g(String(r.app_id))},留空保留原值)`:``,n=await e.tui.modal.openModal({kind:`input`,title:`飞书 App ID`,placeholder:`cli_xxxxxxxxxxxx ${t}`,description:[]});if(n===null)return;o=n||String(r?.app_id??``);let i=await e.tui.modal.openModal({kind:`input`,title:`飞书 App Secret`,mask:!0,description:r?.app_secret?[`留空保留当前 Secret`]:[]});if(i===null)return;if(s=i||String(r?.app_secret??``),!o||!s){e.tui.info.showToast(`plugin-feishu-use`,`App ID 和 App Secret 不能为空,配置已取消。`);return}let a=await e.tui.modal.openModal({kind:`input`,title:`允许的 chat_id(逗号分隔,留空=拒绝所有)`,placeholder:r?`当前: ${r.allowed_chats?.join(`, `)??`空`}`:``,description:[]});if(a===null)return;a?c=a.split(`,`).map(e=>e.trim()).filter(Boolean):r?.allowed_chats&&(c=r.allowed_chats);let u=await e.tui.modal.openModal({kind:`input`,title:`可信 chat_id(逗号分隔,留空=仅只读)`,placeholder:r?`当前: ${r.trusted_chats?.join(`, `)??`空`}`:``,description:[]});if(u===null)return;u?l=u.split(`,`).map(e=>e.trim()).filter(Boolean):r?.trusted_chats&&(l=r.trusted_chats)}let d=r?.workspace_dir?String(r.workspace_dir):i(n,`..`,`..`,`feishu-use-workspace`);u(t,{app_id:o,app_secret:s,allowed_chats:c,trusted_chats:l,workspace_dir:d}),e.tui.info.showToast(`plugin-feishu-use`,`配置已写入 ${t}`);let _=await h(n,o,s);return _?e.tui.info.showToast(`plugin-feishu-use`,`Bot 身份验证成功:${_.name} (${_.openId})`):e.tui.messages.pushMessage({role:`command_echo`,command:`/feishu setup`,status:`error`,summary:`bot 身份验证失败(凭据不正确或网络不可达)`,detail:"请稍后用 `otto feishu config set` 修正凭据,或手动编辑配置文件。"}),{appId:o,botOpenId:_?.openId,botName:_?.name,configPath:t,success:!!_}}async function v(n){let r=n.app.workspaceDir??process.cwd(),i=l(r),a=e({cwd:r,homedir:t()}).find(e=>e.id===`plugin-feishu-use`);if(!a){n.tui.info.showToast(`plugin-feishu-use`,"feishu-use 插件尚未安装,请先在 `/model` 安装入口安装 feishu-use。");return}let o=a.dir;return _(n,i,o,d(i))}export{v as launchFeishuSetup};
6
- //# sourceMappingURL=feishu-setup-B6ETf-8Y.js.map
6
+ //# sourceMappingURL=feishu-setup-5ZoxtH0J.js.map