@yorz/cli 0.9.2 → 0.10.1

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 (134) hide show
  1. package/README.md +27 -44
  2. package/dist/cli/index.js +857 -375
  3. package/dist/cli/serve-launcher.cjs +56 -0
  4. package/dist/gui/assets/{abnfDiagram-VRR7QNED-CvFCe3r_.js → abnfDiagram-VRR7QNED-ChpNHKmG.js} +1 -1
  5. package/dist/{gui-mobile/assets/arc-BBWQwcOX.js → gui/assets/arc-BSWQE9li.js} +1 -1
  6. package/dist/{gui-mobile/assets/architectureDiagram-ZJ3FMSHR-BdDouhRk.js → gui/assets/architectureDiagram-ZJ3FMSHR-BGI0Ue9c.js} +1 -1
  7. package/dist/{gui-mobile/assets/blockDiagram-677ZJIJ3-CyfQcEAp.js → gui/assets/blockDiagram-677ZJIJ3-DzJ0su4P.js} +1 -1
  8. package/dist/gui/assets/{c4Diagram-LMCZKHZV-Cgt15wDO.js → c4Diagram-LMCZKHZV-BijbkJk0.js} +1 -1
  9. package/dist/gui/assets/channel-S36nQ2lf.js +1 -0
  10. package/dist/gui/assets/{chunk-2Q5K7J3B-ByMpzo_7.js → chunk-2Q5K7J3B-Ds8H4SMx.js} +1 -1
  11. package/dist/gui/assets/{chunk-32BRIVSS-LHwTDCaD.js → chunk-32BRIVSS-CaodSuYb.js} +1 -1
  12. package/dist/gui/assets/{chunk-5VM5RSS4-CXrS6z-4.js → chunk-5VM5RSS4-Dk7weLtE.js} +1 -1
  13. package/dist/gui/assets/{chunk-EX3LRPZG-Bw7a0zH3.js → chunk-EX3LRPZG-Dc4TtD4O.js} +1 -1
  14. package/dist/gui/assets/{chunk-JWPE2WC7-wFaiogOx.js → chunk-JWPE2WC7-BXjLPdIx.js} +1 -1
  15. package/dist/gui/assets/{chunk-MOJQB5TN-CC4tXc_1.js → chunk-MOJQB5TN-Ct0R-MTA.js} +1 -1
  16. package/dist/gui/assets/{chunk-RYQCIY6F-BzMRf9sh.js → chunk-RYQCIY6F-BkMMhSkU.js} +1 -1
  17. package/dist/gui/assets/{chunk-V7JOEXUC-DXF9bAuV.js → chunk-V7JOEXUC-CA_5lNDX.js} +1 -1
  18. package/dist/gui/assets/{chunk-VR4S4FIN-DlTRKb1a.js → chunk-VR4S4FIN-BP0qSlug.js} +1 -1
  19. package/dist/gui/assets/{chunk-XXDRQBXY-kVTuzkcL.js → chunk-XXDRQBXY-ChJ7HYsh.js} +1 -1
  20. package/dist/gui/assets/classDiagram-OUVF2IWQ-DfrKIwL2.js +1 -0
  21. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DfrKIwL2.js +1 -0
  22. package/dist/{gui-mobile/assets/cose-bilkent-JH36ORCC-k5DcB7wy.js → gui/assets/cose-bilkent-JH36ORCC-DH_K5WAp.js} +1 -1
  23. package/dist/gui/assets/{cynefin-VYW2F7L2-BKxPBlKv.js → cynefin-VYW2F7L2-BBIjNWwX.js} +1 -1
  24. package/dist/{gui-mobile/assets/cynefinDiagram-TSTJHNR4-CfH5z8jt.js → gui/assets/cynefinDiagram-TSTJHNR4-BRRBlTKf.js} +1 -1
  25. package/dist/{gui-mobile/assets/dagre-VKFMJZFB-WliPW3Tx.js → gui/assets/dagre-VKFMJZFB-CiENng6J.js} +1 -1
  26. package/dist/gui/assets/{diagram-FQU43EPY-DSPwTKxt.js → diagram-FQU43EPY-BpAJWtF0.js} +1 -1
  27. package/dist/{gui-mobile/assets/diagram-G47NLZAW-CJFcYT7j.js → gui/assets/diagram-G47NLZAW-VQzk2Zt3.js} +1 -1
  28. package/dist/gui/assets/{diagram-NH7WQ7WH-Dz_3pmyJ.js → diagram-NH7WQ7WH-BETlyFIV.js} +1 -1
  29. package/dist/{gui-mobile/assets/diagram-OA4YK3LP-c_vARYuP.js → gui/assets/diagram-OA4YK3LP-D7kweveS.js} +1 -1
  30. package/dist/gui/assets/{diagram-WEI45ONY-Dq-7pg1u.js → diagram-WEI45ONY-4xazks6b.js} +1 -1
  31. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-DqkXLemr.js → ebnfDiagram-CCIWWBDH-x6c2P9Gq.js} +1 -1
  32. package/dist/{gui-mobile/assets/erDiagram-Q63AITRT-BosAgKfh.js → gui/assets/erDiagram-Q63AITRT-Dwu_NmfY.js} +1 -1
  33. package/dist/gui/assets/{flowDiagram-23GEKE2U-BItXJm0z.js → flowDiagram-23GEKE2U-mlV2EfUY.js} +1 -1
  34. package/dist/{gui-mobile/assets/ganttDiagram-NO4QXBWP-CJGUZTqX.js → gui/assets/ganttDiagram-NO4QXBWP-sRHt6-K7.js} +1 -1
  35. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-DOTmWDXD.js → gitGraphDiagram-IHSO6WYX-DAmAE7m_.js} +1 -1
  36. package/dist/gui/assets/index-C3kFbt2F.js +34 -0
  37. package/dist/gui/assets/index-CF8GzEat.css +1 -0
  38. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-CH94-RWl.js → infoDiagram-FWYZ7A6U-B4Uf4vez.js} +1 -1
  39. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-DnPsPjhE.js → ishikawaDiagram-FXEZZL3T-BF78753F.js} +1 -1
  40. package/dist/{gui-mobile/assets/journeyDiagram-5HDEW3XC-B4ejke91.js → gui/assets/journeyDiagram-5HDEW3XC-BO6V3zi-.js} +1 -1
  41. package/dist/gui/assets/{kanban-definition-HUTT4EX6-CnEFBR_5.js → kanban-definition-HUTT4EX6-C6Abp-Z0.js} +1 -1
  42. package/dist/gui/assets/{linear-C76lB0Op.js → linear-4_sZdUed.js} +1 -1
  43. package/dist/gui/assets/{mermaid.core-rkuoyu1Y.js → mermaid.core-BoPHxIg3.js} +4 -4
  44. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-Cp4m5M8t.js → mindmap-definition-LN4V7U3C-EcN1F0cO.js} +1 -1
  45. package/dist/gui/assets/{pegDiagram-2B236MQR-OpqvRJRn.js → pegDiagram-2B236MQR-giOW1xob.js} +1 -1
  46. package/dist/gui/assets/{pieDiagram-ENE6RG2P-CA2NTDmg.js → pieDiagram-ENE6RG2P-D9cIRAxf.js} +1 -1
  47. package/dist/{gui-mobile/assets/quadrantDiagram-ABIIQ3AL-CpOioe7x.js → gui/assets/quadrantDiagram-ABIIQ3AL-1jwzcF1N.js} +1 -1
  48. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-D8sNxc-D.js → railroadDiagram-RFXS5EU6-kp1xeMyA.js} +1 -1
  49. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-B_BAewjP.js → requirementDiagram-TGXJPOKE-yspWGQMw.js} +1 -1
  50. package/dist/{gui-mobile/assets/sankeyDiagram-HTMAVEWB-6gqAXG6C.js → gui/assets/sankeyDiagram-HTMAVEWB-Ylm3QxdV.js} +1 -1
  51. package/dist/{gui-mobile/assets/sequenceDiagram-DBY2YBRQ-D-7-OUF6.js → gui/assets/sequenceDiagram-DBY2YBRQ-CJJnMk2d.js} +1 -1
  52. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-uA8UtQtp.js → sizeCapture-X5ZJPWSS-F3cALp3d.js} +1 -1
  53. package/dist/{gui-mobile/assets/stateDiagram-2N3HPSRC-CBem3dqE.js → gui/assets/stateDiagram-2N3HPSRC-D7aZ2O-l.js} +1 -1
  54. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-D8vtVO10.js +1 -0
  55. package/dist/{gui-mobile/assets/swimlanes-5IMT3BWC-CTxpjyu6.js → gui/assets/swimlanes-5IMT3BWC-UrlubmXy.js} +2 -2
  56. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BAhohxIt.js +8 -0
  57. package/dist/{gui-mobile/assets/timeline-definition-FHXFAJF6-sxn38dUp.js → gui/assets/timeline-definition-FHXFAJF6-Bl_hCpye.js} +1 -1
  58. package/dist/gui/assets/{vennDiagram-L72KCM5P-B29R19dw.js → vennDiagram-L72KCM5P-CvGLve2y.js} +1 -1
  59. package/dist/gui/assets/{wardleyDiagram-EHGQE667-B5eqpyg8.js → wardleyDiagram-EHGQE667-DLRGPeZX.js} +1 -1
  60. package/dist/{gui-mobile/assets/xychartDiagram-FW5EYKEG-AmEku7SD.js → gui/assets/xychartDiagram-FW5EYKEG-bB6p1K59.js} +1 -1
  61. package/dist/gui/index.html +2 -2
  62. package/dist/gui-mobile/assets/{abnfDiagram-VRR7QNED-dO2fe8On.js → abnfDiagram-VRR7QNED-Cii3YIsO.js} +1 -1
  63. package/dist/{gui/assets/arc-BR65Sp8N.js → gui-mobile/assets/arc-D5zxWTc6.js} +1 -1
  64. package/dist/{gui/assets/architectureDiagram-ZJ3FMSHR-AiVGAt8P.js → gui-mobile/assets/architectureDiagram-ZJ3FMSHR-BUrFMtmN.js} +1 -1
  65. package/dist/{gui/assets/blockDiagram-677ZJIJ3-HuFROz1l.js → gui-mobile/assets/blockDiagram-677ZJIJ3-CLXT0p5K.js} +1 -1
  66. package/dist/gui-mobile/assets/{c4Diagram-LMCZKHZV-BDwCd8AJ.js → c4Diagram-LMCZKHZV-XZDjR7Pf.js} +1 -1
  67. package/dist/gui-mobile/assets/channel-DAACRh1k.js +1 -0
  68. package/dist/gui-mobile/assets/{chunk-2Q5K7J3B-C21jFDT7.js → chunk-2Q5K7J3B-C0SSdrgO.js} +1 -1
  69. package/dist/gui-mobile/assets/{chunk-32BRIVSS-DYcgMwLC.js → chunk-32BRIVSS-B-l29SGQ.js} +1 -1
  70. package/dist/gui-mobile/assets/{chunk-5VM5RSS4-DHUChIkE.js → chunk-5VM5RSS4-2Oc13ID8.js} +1 -1
  71. package/dist/gui-mobile/assets/{chunk-EX3LRPZG-BsiOO0cg.js → chunk-EX3LRPZG-tRiKUhHg.js} +1 -1
  72. package/dist/gui-mobile/assets/{chunk-JWPE2WC7-DwRFcp8M.js → chunk-JWPE2WC7-CWxLnmGK.js} +1 -1
  73. package/dist/gui-mobile/assets/{chunk-MOJQB5TN-CZAIa8xj.js → chunk-MOJQB5TN-CRo9axdS.js} +1 -1
  74. package/dist/gui-mobile/assets/{chunk-RYQCIY6F-CjiSByKd.js → chunk-RYQCIY6F-BWk2tC2V.js} +1 -1
  75. package/dist/gui-mobile/assets/{chunk-V7JOEXUC-C5X5gobg.js → chunk-V7JOEXUC-CAf_TQ8c.js} +1 -1
  76. package/dist/gui-mobile/assets/{chunk-VR4S4FIN-BG4Ho3oM.js → chunk-VR4S4FIN-Djvb-G48.js} +1 -1
  77. package/dist/gui-mobile/assets/{chunk-XXDRQBXY-Cy55QGn1.js → chunk-XXDRQBXY-DcdCewNm.js} +1 -1
  78. package/dist/gui-mobile/assets/classDiagram-OUVF2IWQ-k67r25Hl.js +1 -0
  79. package/dist/gui-mobile/assets/classDiagram-v2-EOCWNBFH-k67r25Hl.js +1 -0
  80. package/dist/{gui/assets/cose-bilkent-JH36ORCC-BAqcUvRT.js → gui-mobile/assets/cose-bilkent-JH36ORCC-DQd0ag_q.js} +1 -1
  81. package/dist/gui-mobile/assets/{cynefin-VYW2F7L2-D0zs5vNv.js → cynefin-VYW2F7L2-O3JIjwWq.js} +1 -1
  82. package/dist/{gui/assets/cynefinDiagram-TSTJHNR4-B30nmSKx.js → gui-mobile/assets/cynefinDiagram-TSTJHNR4-Ca9ChAoZ.js} +1 -1
  83. package/dist/{gui/assets/dagre-VKFMJZFB-B_6xJi_I.js → gui-mobile/assets/dagre-VKFMJZFB-Dz84jzyH.js} +1 -1
  84. package/dist/gui-mobile/assets/{diagram-FQU43EPY-CdurYY3r.js → diagram-FQU43EPY-Ddl2aZsD.js} +1 -1
  85. package/dist/{gui/assets/diagram-G47NLZAW-CE6kku8E.js → gui-mobile/assets/diagram-G47NLZAW-BUUkAM1L.js} +1 -1
  86. package/dist/gui-mobile/assets/{diagram-NH7WQ7WH-Covs7EOO.js → diagram-NH7WQ7WH-Bhp3-t7h.js} +1 -1
  87. package/dist/{gui/assets/diagram-OA4YK3LP-9K6njZVx.js → gui-mobile/assets/diagram-OA4YK3LP-Dz2X8j8T.js} +1 -1
  88. package/dist/gui-mobile/assets/{diagram-WEI45ONY-B8DLNiMy.js → diagram-WEI45ONY-m28jBgF4.js} +1 -1
  89. package/dist/gui-mobile/assets/{ebnfDiagram-CCIWWBDH-dOuQRK-3.js → ebnfDiagram-CCIWWBDH-Dh-_tj-T.js} +1 -1
  90. package/dist/{gui/assets/erDiagram-Q63AITRT-DwCKeJjN.js → gui-mobile/assets/erDiagram-Q63AITRT-C-3poaMf.js} +1 -1
  91. package/dist/gui-mobile/assets/{flowDiagram-23GEKE2U-HWFThTdv.js → flowDiagram-23GEKE2U-BopsgXAH.js} +1 -1
  92. package/dist/{gui/assets/ganttDiagram-NO4QXBWP-B7F8aBqd.js → gui-mobile/assets/ganttDiagram-NO4QXBWP-Bn7GJbzm.js} +1 -1
  93. package/dist/gui-mobile/assets/{gitGraphDiagram-IHSO6WYX-D1j9Ff_z.js → gitGraphDiagram-IHSO6WYX-DfLUxTzH.js} +1 -1
  94. package/dist/gui-mobile/assets/{index-BWC60m5U.js → index--agUQ6GR.js} +16 -16
  95. package/dist/gui-mobile/assets/index-CNDPS9z2.css +1 -0
  96. package/dist/gui-mobile/assets/{infoDiagram-FWYZ7A6U-5Gq_R5ny.js → infoDiagram-FWYZ7A6U-nrBaoqsE.js} +1 -1
  97. package/dist/gui-mobile/assets/{ishikawaDiagram-FXEZZL3T-DZAckb0K.js → ishikawaDiagram-FXEZZL3T-DEVq4aJ1.js} +1 -1
  98. package/dist/{gui/assets/journeyDiagram-5HDEW3XC-IxNPj9DL.js → gui-mobile/assets/journeyDiagram-5HDEW3XC-CA-AnD2i.js} +1 -1
  99. package/dist/gui-mobile/assets/{kanban-definition-HUTT4EX6-CKIlESkz.js → kanban-definition-HUTT4EX6-BtUD4217.js} +1 -1
  100. package/dist/gui-mobile/assets/{linear-DjghM5B6.js → linear-Cs1nDCen.js} +1 -1
  101. package/dist/gui-mobile/assets/{mermaid.core-Cery4OJZ.js → mermaid.core-CjlnyTvi.js} +4 -4
  102. package/dist/gui-mobile/assets/{mindmap-definition-LN4V7U3C-Bb9PmQcE.js → mindmap-definition-LN4V7U3C-DpHq0ntz.js} +1 -1
  103. package/dist/gui-mobile/assets/{pegDiagram-2B236MQR-Dk6yxZPA.js → pegDiagram-2B236MQR-UB1lVTuX.js} +1 -1
  104. package/dist/gui-mobile/assets/{pieDiagram-ENE6RG2P-9qPsJCKZ.js → pieDiagram-ENE6RG2P-7qkPFOFY.js} +1 -1
  105. package/dist/{gui/assets/quadrantDiagram-ABIIQ3AL-1wgAkd5_.js → gui-mobile/assets/quadrantDiagram-ABIIQ3AL-DPM04b8X.js} +1 -1
  106. package/dist/gui-mobile/assets/{railroadDiagram-RFXS5EU6-CYnrq0_H.js → railroadDiagram-RFXS5EU6-BY7DXXsK.js} +1 -1
  107. package/dist/gui-mobile/assets/{requirementDiagram-TGXJPOKE-Dan1m7mP.js → requirementDiagram-TGXJPOKE-RJuz8ezU.js} +1 -1
  108. package/dist/{gui/assets/sankeyDiagram-HTMAVEWB-D5yRszAY.js → gui-mobile/assets/sankeyDiagram-HTMAVEWB-DIHqskb-.js} +1 -1
  109. package/dist/{gui/assets/sequenceDiagram-DBY2YBRQ-xLRttR6a.js → gui-mobile/assets/sequenceDiagram-DBY2YBRQ-B91bF6fy.js} +1 -1
  110. package/dist/gui-mobile/assets/{sizeCapture-X5ZJPWSS-Cf4atOsc.js → sizeCapture-X5ZJPWSS-Bb8LgAbm.js} +1 -1
  111. package/dist/{gui/assets/stateDiagram-2N3HPSRC-JLfWMf-y.js → gui-mobile/assets/stateDiagram-2N3HPSRC-ef4n7bGq.js} +1 -1
  112. package/dist/gui-mobile/assets/stateDiagram-v2-6OUMAXLB-D5KNiKni.js +1 -0
  113. package/dist/{gui/assets/swimlanes-5IMT3BWC-DzNlggdg.js → gui-mobile/assets/swimlanes-5IMT3BWC-ZdNv29bw.js} +2 -2
  114. package/dist/gui-mobile/assets/swimlanesDiagram-G3AALYLV-CTHOZVwF.js +8 -0
  115. package/dist/{gui/assets/timeline-definition-FHXFAJF6-DisxBENp.js → gui-mobile/assets/timeline-definition-FHXFAJF6-fXhjYYU8.js} +1 -1
  116. package/dist/gui-mobile/assets/{vennDiagram-L72KCM5P-BI_Xsfbx.js → vennDiagram-L72KCM5P-BxTJzmPG.js} +1 -1
  117. package/dist/gui-mobile/assets/{wardleyDiagram-EHGQE667-1DzhpDmv.js → wardleyDiagram-EHGQE667-CiAx3NHL.js} +1 -1
  118. package/dist/{gui/assets/xychartDiagram-FW5EYKEG-BFhk2kj2.js → gui-mobile/assets/xychartDiagram-FW5EYKEG-OM8YLeYP.js} +1 -1
  119. package/dist/gui-mobile/index.html +2 -2
  120. package/dist/gui-mobile/sw.js +1 -1
  121. package/package.json +3 -2
  122. package/dist/gui/assets/channel-DWjIfFiO.js +0 -1
  123. package/dist/gui/assets/classDiagram-OUVF2IWQ-I21k2As-.js +0 -1
  124. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-I21k2As-.js +0 -1
  125. package/dist/gui/assets/index-CkGO8Fpq.js +0 -34
  126. package/dist/gui/assets/index-e5JFp5J1.css +0 -1
  127. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-m9lGgZDF.js +0 -1
  128. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-67oDFDZ3.js +0 -8
  129. package/dist/gui-mobile/assets/channel-BH91jO0g.js +0 -1
  130. package/dist/gui-mobile/assets/classDiagram-OUVF2IWQ-DI_HWJF7.js +0 -1
  131. package/dist/gui-mobile/assets/classDiagram-v2-EOCWNBFH-DI_HWJF7.js +0 -1
  132. package/dist/gui-mobile/assets/index-BABBu2VR.css +0 -1
  133. package/dist/gui-mobile/assets/stateDiagram-v2-6OUMAXLB-BQUCaoqO.js +0 -1
  134. package/dist/gui-mobile/assets/swimlanesDiagram-G3AALYLV-B6OThFgb.js +0 -8
package/dist/cli/index.js CHANGED
@@ -2,16 +2,16 @@
2
2
  import { homedir, platform } from "node:os";
3
3
  import { Command, Option } from "commander";
4
4
  import { stat, mkdir, writeFile, rename, readFile, rm, access, cp, readdir, appendFile, copyFile } from "node:fs/promises";
5
- import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, relative, sep, normalize } from "node:path";
5
+ import { posix, win32, join, isAbsolute, resolve, basename as basename$1, dirname, extname, relative, sep, normalize } from "node:path";
6
6
  import { createHash, randomUUID, randomBytes } from "node:crypto";
7
- import { existsSync, openSync, closeSync, statSync, readSync, mkdirSync, watch, createReadStream } from "node:fs";
7
+ import { existsSync, constants, openSync, closeSync, statSync, readSync, mkdirSync, watch, createReadStream } from "node:fs";
8
8
  import { execFile, spawn, execFileSync } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
+ import { fileURLToPath } from "node:url";
10
11
  import { serve } from "@hono/node-server";
11
12
  import { isIP } from "node:net";
12
13
  import { Hono } from "hono";
13
14
  import { streamSSE } from "hono/streaming";
14
- import { fileURLToPath } from "node:url";
15
15
  import trash from "trash";
16
16
  import matter from "gray-matter";
17
17
  import chokidar from "chokidar";
@@ -20,14 +20,15 @@ import { listSessions, getSessionMessages, query } from "@anthropic-ai/claude-ag
20
20
  import { createInterface } from "node:readline";
21
21
  import { Codex } from "@openai/codex-sdk";
22
22
  import { createOpencode } from "@opencode-ai/sdk";
23
+ import { ModelRuntime, SessionManager as SessionManager$1, createAgentSession } from "@earendil-works/pi-coding-agent";
23
24
  import { createInterface as createInterface$1 } from "node:readline/promises";
24
25
  import MarkdownIt from "markdown-it";
25
26
  const name = "@yorz/cli";
26
- const version = "0.9.2";
27
+ const version = "0.10.1";
27
28
  const type = "module";
28
29
  const description = "";
29
30
  const bin = { "yorz": "./dist/cli/index.js" };
30
- const engines = { "node": ">=20" };
31
+ const engines = { "node": ">=22.19.0" };
31
32
  const files = ["dist", "README.md"];
32
33
  const keywords = ["ai", "agent", "spec-driven", "decision-os", "claude-code", "opencode", "workflow", "cli"];
33
34
  const license = "LGPL-3.0-or-later";
@@ -35,7 +36,7 @@ const repository = { "type": "git", "url": "git+https://github.com/hughfenghen/Y
35
36
  const homepage = "https://github.com/hughfenghen/YorZ#readme";
36
37
  const publishConfig = { "access": "public" };
37
38
  const scripts = { "build": "pnpm run build:cli && pnpm run build:gui && pnpm run build:gui-mobile", "build:cli": "vite build", "build:gui": "vite build --config vite.gui.config.ts", "build:gui-mobile": "vite build --config vite.gui-mobile.config.ts", "dev:cli": "pnpm run build:cli && node dist/cli/index.js serve --port 7424 --foreground", "dev:gui": "vite --config vite.gui.config.ts", "dev:gui-mobile": "vite --config vite.gui-mobile.config.ts", "gen:mobile-icons": "node scripts/gen-mobile-icons.mjs", "typecheck": "tsc -b", "test": "vitest run", "test:agent": "node scripts/test-agent.mjs", "test:e2e": "pnpm run build && playwright test", "format": 'prettier --write "src/**/*.{ts,tsx,md,css}"', "prepare": "node scripts/setup-git-hooks.mjs", "prepublishOnly": "pnpm run build" };
38
- const dependencies = { "@anthropic-ai/claude-agent-sdk": "^0.3.207", "@hono/node-server": "^2.0.4", "@openai/codex-sdk": "^0.144.1", "@opencode-ai/sdk": "^1.17.18", "chokidar": "^5.0.0", "commander": "^12.1.0", "gray-matter": "^4.0.3", "hono": "^4.12.25", "jsdom": "^25.0.1", "markdown-it": "^14.2.0", "mermaid": "^11.0.0", "morphdom": "^2.7.8", "parse-diff": "^0.12.0", "timeago.js": "^4.0.2", "trash": "^10.1.1" };
39
+ const dependencies = { "@anthropic-ai/claude-agent-sdk": "^0.3.207", "@earendil-works/pi-coding-agent": "^0.85.1", "@hono/node-server": "^2.0.4", "@openai/codex-sdk": "^0.144.1", "@opencode-ai/sdk": "^1.17.18", "chokidar": "^5.0.0", "commander": "^12.1.0", "gray-matter": "^4.0.3", "hono": "^4.12.25", "jsdom": "^25.0.1", "markdown-it": "^14.2.0", "mermaid": "^11.0.0", "morphdom": "^2.7.8", "parse-diff": "^0.12.0", "timeago.js": "^4.0.2", "trash": "^10.1.1" };
39
40
  const devDependencies = { "@fontsource/jetbrains-mono": "^5.3.0", "@kobalte/core": "^0.13.12", "@playwright/test": "^1.61.0", "@solidjs/router": "^0.16.1", "@types/jsdom": "^21.1.7", "@types/markdown-it": "^14.1.2", "@types/node": "^22.10.0", "autoprefixer": "^10.5.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.1.0", "highlight.js": "^11.11.1", "i18next": "^26.3.6", "i18next-browser-languagedetector": "^8.2.1", "lucide-solid": "^1.24.0", "markdown-it-task-lists": "^2.1.1", "postcss": "^8.5.16", "prettier": "^3.4.0", "solid-js": "^1.9.13", "tailwind-merge": "^3.6.0", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.0", "vite": "^6.0.0", "vite-plugin-pwa": "^1.3.0", "vite-plugin-solid": "^2.11.12", "vitest": "^2.1.0", "workbox-window": "^7.4.1" };
40
41
  const pkg = {
41
42
  name,
@@ -141,6 +142,20 @@ function mergeCustomInstructions(project, global) {
141
142
  const names = new Set(project.map((item) => item.name));
142
143
  return [...project, ...global.filter((item) => !names.has(item.name))];
143
144
  }
145
+ function normalizeAbsPath(input, platform2 = process.platform, cwd = process.cwd()) {
146
+ const trimmed = input.trim();
147
+ if (platform2 !== "win32") return posix.resolve(cwd, trimmed);
148
+ const resolved = win32.resolve(cwd, trimmed);
149
+ return upperDriveLetter(resolved);
150
+ }
151
+ function samePath(a, b, platform2 = process.platform) {
152
+ if (platform2 !== "win32") return a === b;
153
+ return a.toLowerCase() === b.toLowerCase();
154
+ }
155
+ function upperDriveLetter(p) {
156
+ if (/^[a-z]:/.test(p)) return p[0].toUpperCase() + p.slice(1);
157
+ return p;
158
+ }
144
159
  const CURRENT_VERSION$1 = 1;
145
160
  const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
146
161
  const DEFAULT_NOTIFICATIONS = {
@@ -264,7 +279,9 @@ function normalizeAgent$1(value) {
264
279
  if (!value || typeof value !== "object") return { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind };
265
280
  const obj = value;
266
281
  const defaultKind = obj.defaultKind;
267
- if (defaultKind === "opencode" || defaultKind === "codex") return { defaultKind };
282
+ if (defaultKind === "opencode" || defaultKind === "codex" || defaultKind === "pi") {
283
+ return { defaultKind };
284
+ }
268
285
  return { defaultKind: "claude" };
269
286
  }
270
287
  function normalizeNotifications(value) {
@@ -391,9 +408,9 @@ function generateProjectId(absPath, depth = 0) {
391
408
  function slugify(text) {
392
409
  return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
393
410
  }
394
- async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
411
+ async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date(), platform2 = process.platform) {
395
412
  const config = await loadGlobalConfig(filePath);
396
- const existing = config.projects.find((p) => p.path === absPath);
413
+ const existing = config.projects.find((p) => samePath(p.path, absPath, platform2));
397
414
  if (existing) return { entry: existing, created: false };
398
415
  const entry = {
399
416
  id: generateProjectId(absPath),
@@ -507,13 +524,10 @@ async function isGitRepo(cwd) {
507
524
  return false;
508
525
  }
509
526
  }
510
- async function runGitInit(cwd) {
527
+ async function runGitInit(cwd, opts = {}) {
528
+ const stdio = opts.stdio ?? "inherit";
511
529
  await new Promise((resolve2, reject) => {
512
- const child = spawn(
513
- "git",
514
- ["init"],
515
- withHiddenWindowsConsole({ cwd, stdio: "inherit" })
516
- );
530
+ const child = spawn("git", ["init"], withHiddenWindowsConsole({ cwd, stdio }));
517
531
  child.on("error", reject);
518
532
  child.on("exit", (code) => {
519
533
  if (code === 0) resolve2();
@@ -521,6 +535,34 @@ async function runGitInit(cwd) {
521
535
  });
522
536
  });
523
537
  }
538
+ async function ensureTmpIgnored(cwd) {
539
+ if (!await isGitRepo(cwd)) return null;
540
+ const giPath = join(cwd, ".gitignore");
541
+ let existing = "";
542
+ try {
543
+ existing = await readFile(giPath, "utf8");
544
+ } catch {
545
+ existing = "";
546
+ }
547
+ if (hasIgnoreEntry(existing, ".yorz/tmp")) {
548
+ return { updated: false, path: giPath };
549
+ }
550
+ const needsNewline = existing.length > 0 && !existing.endsWith("\n");
551
+ const next = `${existing}${needsNewline ? "\n" : ""}.yorz/tmp
552
+ `;
553
+ await writeFile(giPath, next, "utf8");
554
+ return { updated: true, path: giPath };
555
+ }
556
+ function hasIgnoreEntry(content, target) {
557
+ const normalized = target.replace(/\/$/, "");
558
+ for (const rawLine of content.split("\n")) {
559
+ const line = rawLine.trim();
560
+ if (!line || line.startsWith("#")) continue;
561
+ const cleaned = line.replace(/\/$/, "").replace(/^\//, "");
562
+ if (cleaned === normalized) return true;
563
+ }
564
+ return false;
565
+ }
524
566
  const SKILL_DIR_NAME = "yorz-spec";
525
567
  const SKILL_DIR_NAMES = ["yorz-spec", "yorz-debug", "yorz-git-ops"];
526
568
  const SKILL_FILES = /* @__PURE__ */ Object.assign({
@@ -705,34 +747,6 @@ async function dirExists(path) {
705
747
  return false;
706
748
  }
707
749
  }
708
- function hasIgnoreEntry(content, target) {
709
- const normalized = target.replace(/\/$/, "");
710
- for (const rawLine of content.split("\n")) {
711
- const line = rawLine.trim();
712
- if (!line || line.startsWith("#")) continue;
713
- const cleaned = line.replace(/\/$/, "").replace(/^\//, "");
714
- if (cleaned === normalized) return true;
715
- }
716
- return false;
717
- }
718
- async function ensureTmpIgnored(cwd) {
719
- if (!await isGitRepo(cwd)) return null;
720
- const giPath = join(cwd, ".gitignore");
721
- let existing = "";
722
- try {
723
- existing = await readFile(giPath, "utf8");
724
- } catch {
725
- existing = "";
726
- }
727
- if (hasIgnoreEntry(existing, ".yorz/tmp")) {
728
- return { updated: false, path: giPath };
729
- }
730
- const needsNewline = existing.length > 0 && !existing.endsWith("\n");
731
- const next = `${existing}${needsNewline ? "\n" : ""}.yorz/tmp
732
- `;
733
- await writeFile(giPath, next, "utf8");
734
- return { updated: true, path: giPath };
735
- }
736
750
  async function uninstall(opts = {}) {
737
751
  const baseDir = opts.skillsDir ?? resolveGlobalSkillsDir();
738
752
  const results = [];
@@ -1479,39 +1493,50 @@ function findProjectRoot(startDir) {
1479
1493
  function normalizeUsage(kind, raw) {
1480
1494
  if (!raw || typeof raw !== "object") return void 0;
1481
1495
  const obj = raw;
1482
- const snapshot = kind === "codex" ? fromCodex(obj) : kind === "opencode" ? fromOpenCode(obj) : fromClaude(obj);
1496
+ const snapshot = kind === "codex" ? fromCodex(obj) : kind === "opencode" ? fromOpenCode(obj) : kind === "pi" ? fromPi(obj) : fromClaude(obj);
1483
1497
  return isEmpty(snapshot) ? void 0 : snapshot;
1484
1498
  }
1485
1499
  function fromClaude(u) {
1486
1500
  return compact({
1487
- inputTokens: num$2(u.input_tokens),
1488
- outputTokens: num$2(u.output_tokens),
1489
- cacheReadTokens: num$2(u.cache_read_input_tokens),
1490
- cacheCreateTokens: num$2(u.cache_creation_input_tokens)
1501
+ inputTokens: num$3(u.input_tokens),
1502
+ outputTokens: num$3(u.output_tokens),
1503
+ cacheReadTokens: num$3(u.cache_read_input_tokens),
1504
+ cacheCreateTokens: num$3(u.cache_creation_input_tokens)
1491
1505
  });
1492
1506
  }
1493
1507
  function fromCodex(u) {
1494
- const total = num$2(u.input_tokens);
1495
- const cached = num$2(u.cached_input_tokens);
1508
+ const total = num$3(u.input_tokens);
1509
+ const cached = num$3(u.cached_input_tokens);
1496
1510
  const uncached = total !== void 0 && cached !== void 0 ? Math.max(total - cached, 0) : total;
1497
1511
  return compact({
1498
1512
  inputTokens: uncached,
1499
1513
  cacheReadTokens: cached,
1500
- outputTokens: num$2(u.output_tokens),
1501
- reasoningTokens: num$2(u.reasoning_output_tokens)
1514
+ outputTokens: num$3(u.output_tokens),
1515
+ reasoningTokens: num$3(u.reasoning_output_tokens)
1502
1516
  });
1503
1517
  }
1504
1518
  function fromOpenCode(u) {
1505
1519
  const cache = u.cache && typeof u.cache === "object" ? u.cache : {};
1506
1520
  return compact({
1507
- inputTokens: num$2(u.input),
1508
- outputTokens: num$2(u.output),
1509
- reasoningTokens: num$2(u.reasoning),
1510
- cacheReadTokens: num$2(cache.read),
1511
- cacheCreateTokens: num$2(cache.write)
1521
+ inputTokens: num$3(u.input),
1522
+ outputTokens: num$3(u.output),
1523
+ reasoningTokens: num$3(u.reasoning),
1524
+ cacheReadTokens: num$3(cache.read),
1525
+ cacheCreateTokens: num$3(cache.write)
1512
1526
  });
1513
1527
  }
1514
- function num$2(value) {
1528
+ function fromPi(u) {
1529
+ const cost = u.cost && typeof u.cost === "object" ? u.cost : {};
1530
+ return compact({
1531
+ inputTokens: num$3(u.input),
1532
+ outputTokens: num$3(u.output),
1533
+ cacheReadTokens: num$3(u.cacheRead),
1534
+ cacheCreateTokens: num$3(u.cacheWrite),
1535
+ reasoningTokens: num$3(u.reasoning),
1536
+ costUsd: num$3(cost.total)
1537
+ });
1538
+ }
1539
+ function num$3(value) {
1515
1540
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1516
1541
  }
1517
1542
  function compact(snapshot) {
@@ -2344,6 +2369,7 @@ function normalizeAgent(value) {
2344
2369
  if (typeof value === "string") {
2345
2370
  if (value === "opencode") return { kind: "opencode" };
2346
2371
  if (value === "codex") return { kind: "codex" };
2372
+ if (value === "pi") return { kind: "pi" };
2347
2373
  return { kind: "claude" };
2348
2374
  }
2349
2375
  if (!value || typeof value !== "object") return { kind: "claude" };
@@ -2352,12 +2378,7 @@ function normalizeAgent(value) {
2352
2378
  if (kind === "inherit") return { kind: "inherit" };
2353
2379
  if (kind === "opencode") return { kind: "opencode" };
2354
2380
  if (kind === "codex") return { kind: "codex" };
2355
- if (kind === "custom") {
2356
- const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
2357
- if (!cmd) return { kind: "claude" };
2358
- const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
2359
- return { kind: "custom", cmd, args };
2360
- }
2381
+ if (kind === "pi") return { kind: "pi" };
2361
2382
  return { kind: "claude" };
2362
2383
  }
2363
2384
  function normalizeSpecsDir(value) {
@@ -2373,7 +2394,7 @@ async function loadCustomInstructions(projectPath) {
2373
2394
  ]);
2374
2395
  return mergeCustomInstructions(project, global);
2375
2396
  }
2376
- const KINDS = ["claude", "codex", "opencode"];
2397
+ const KINDS = ["claude", "codex", "opencode", "pi"];
2377
2398
  const DRAFT_ID_RE = /^[a-zA-Z0-9-]{1,64}$/;
2378
2399
  function buildChatPrompt(prompt, draftId, attachments) {
2379
2400
  if (attachments.length === 0) return prompt;
@@ -3427,6 +3448,39 @@ function createEventsRoutes(resolveProject, registry, projectsBus, systemNotific
3427
3448
  });
3428
3449
  return app;
3429
3450
  }
3451
+ class NeedGitInitError extends Error {
3452
+ path;
3453
+ constructor(absPath) {
3454
+ super(`target directory is not a git repository: ${absPath}`);
3455
+ this.name = "NeedGitInitError";
3456
+ this.path = absPath;
3457
+ }
3458
+ }
3459
+ async function addProjectWithGit(opts) {
3460
+ const platform2 = opts.platform ?? process.platform;
3461
+ if (typeof opts.path !== "string" || !opts.path.trim()) {
3462
+ throw new Error("path required");
3463
+ }
3464
+ const abs = normalizeAbsPath(opts.path, platform2, opts.cwd);
3465
+ if (!existsSync(abs)) {
3466
+ throw new Error(`path does not exist: ${abs}`);
3467
+ }
3468
+ const stats = await stat(abs);
3469
+ if (!stats.isDirectory()) {
3470
+ throw new Error(`path is not a directory: ${abs}`);
3471
+ }
3472
+ let gitInitialized = false;
3473
+ if (!await isGitRepo(abs)) {
3474
+ if (!opts.gitInit) throw new NeedGitInitError(abs);
3475
+ const init = opts.runGitInit ?? ((cwd) => runGitInit(cwd, { stdio: "ignore" }));
3476
+ await init(abs);
3477
+ gitInitialized = true;
3478
+ }
3479
+ await prepareProjectDir(abs, opts.cwd);
3480
+ const gitignore = await ensureTmpIgnored(abs);
3481
+ const { entry, created } = await addProject(abs, opts.globalConfigPath, void 0, platform2);
3482
+ return { entry, created, gitInitialized, gitignore };
3483
+ }
3430
3484
  function createProjectRoutes(registry, worktreeManager) {
3431
3485
  const app = new Hono();
3432
3486
  app.get("/projects", async (c) => {
@@ -3440,7 +3494,8 @@ function createProjectRoutes(registry, worktreeManager) {
3440
3494
  } catch {
3441
3495
  return c.json({ error: "invalid JSON body" }, 400);
3442
3496
  }
3443
- const path = body?.path;
3497
+ const raw = body;
3498
+ const path = raw?.path;
3444
3499
  if (typeof path !== "string" || !path.trim()) {
3445
3500
  return c.json({ error: "path required" }, 400);
3446
3501
  }
@@ -3448,7 +3503,7 @@ function createProjectRoutes(registry, worktreeManager) {
3448
3503
  if (!isAbsolute(trimmed)) {
3449
3504
  return c.json({ error: "path must be absolute" }, 400);
3450
3505
  }
3451
- const normalized = resolve(trimmed);
3506
+ const normalized = normalizeAbsPath(trimmed);
3452
3507
  if (!existsSync(normalized)) {
3453
3508
  return c.json({ error: `path does not exist: ${normalized}` }, 400);
3454
3509
  }
@@ -3457,7 +3512,7 @@ function createProjectRoutes(registry, worktreeManager) {
3457
3512
  return c.json({ error: `path is not a directory: ${normalized}` }, 400);
3458
3513
  }
3459
3514
  try {
3460
- const result = await registry.add(normalized);
3515
+ const result = await registry.addWithGit(normalized, { gitInit: raw?.gitInit === true });
3461
3516
  return c.json(
3462
3517
  {
3463
3518
  id: result.entry.id,
@@ -3468,6 +3523,9 @@ function createProjectRoutes(registry, worktreeManager) {
3468
3523
  result.created ? 201 : 200
3469
3524
  );
3470
3525
  } catch (err) {
3526
+ if (err instanceof NeedGitInitError) {
3527
+ return c.json({ error: err.message, needGitInit: true, path: err.path }, 409);
3528
+ }
3471
3529
  return c.json({ error: err.message }, 400);
3472
3530
  }
3473
3531
  });
@@ -3595,27 +3653,8 @@ function parseAgent(value) {
3595
3653
  if (kind === "claude") return { value: { kind: "claude" } };
3596
3654
  if (kind === "opencode") return { value: { kind: "opencode" } };
3597
3655
  if (kind === "codex") return { value: { kind: "codex" } };
3598
- if (kind === "custom") {
3599
- const cmd = obj.cmd;
3600
- if (typeof cmd !== "string" || !cmd.trim()) {
3601
- return { error: "agent.cmd required when kind=custom" };
3602
- }
3603
- if (cmd.length > 500) return { error: "agent.cmd too long (max 500)" };
3604
- const argsRaw = obj.args;
3605
- let args = [];
3606
- if (argsRaw !== void 0) {
3607
- if (!Array.isArray(argsRaw)) return { error: "agent.args must be an array of strings" };
3608
- args = [];
3609
- for (const a of argsRaw) {
3610
- if (typeof a !== "string") return { error: "agent.args entries must be strings" };
3611
- if (a.length > 500) return { error: "agent.args entry too long (max 500)" };
3612
- args.push(a);
3613
- }
3614
- if (args.length > 64) return { error: "agent.args too many entries (max 64)" };
3615
- }
3616
- return { value: { kind: "custom", cmd: cmd.trim(), args } };
3617
- }
3618
- return { error: "agent.kind must be inherit | claude | opencode | codex | custom" };
3656
+ if (kind === "pi") return { value: { kind: "pi" } };
3657
+ return { error: "agent.kind must be inherit | claude | opencode | codex | pi" };
3619
3658
  }
3620
3659
  function parseSpecsDir(value) {
3621
3660
  const fallback = defaultProjectConfig().specsDir;
@@ -3820,8 +3859,8 @@ function parseBody(value) {
3820
3859
  }
3821
3860
  const agentObj = agent;
3822
3861
  const defaultKind = agentObj.defaultKind;
3823
- if (defaultKind !== "claude" && defaultKind !== "opencode" && defaultKind !== "codex") {
3824
- return { error: "agent.defaultKind must be claude | opencode | codex" };
3862
+ if (defaultKind !== "claude" && defaultKind !== "opencode" && defaultKind !== "codex" && defaultKind !== "pi") {
3863
+ return { error: "agent.defaultKind must be claude | opencode | codex | pi" };
3825
3864
  }
3826
3865
  const notifications = obj.notifications;
3827
3866
  if (!notifications || typeof notifications !== "object") {
@@ -4238,6 +4277,172 @@ function createProjectFilesRoutes(resolveProject) {
4238
4277
  });
4239
4278
  return app;
4240
4279
  }
4280
+ const MAX_ENTRIES = 1e3;
4281
+ const WIN_DRIVE_ROOT = "";
4282
+ function createFsRoutes(deps = {}) {
4283
+ const platform2 = deps.platform ?? process.platform;
4284
+ const homedir$1 = deps.homedir ?? homedir;
4285
+ const isWin = platform2 === "win32";
4286
+ const p = isWin ? win32 : posix;
4287
+ const app = new Hono();
4288
+ app.get("/fs/list", async (c) => {
4289
+ const rawPath = c.req.query("path");
4290
+ const showHidden = c.req.query("showHidden") === "1";
4291
+ const home = normalizeAbsPath(homedir$1(), platform2);
4292
+ const blank = rawPath === void 0 || rawPath.trim() === "";
4293
+ if (isWin && blank) {
4294
+ const drives = await listWindowsDrives();
4295
+ return c.json({
4296
+ path: WIN_DRIVE_ROOT,
4297
+ parent: null,
4298
+ sep: p.sep,
4299
+ home,
4300
+ entries: drives,
4301
+ truncated: false
4302
+ });
4303
+ }
4304
+ const target = blank ? home : rawPath;
4305
+ if (!p.isAbsolute(target.trim())) {
4306
+ return c.json({ error: "path must be absolute" }, 400);
4307
+ }
4308
+ const dir = normalizeAbsPath(target, platform2);
4309
+ let stats;
4310
+ try {
4311
+ stats = await stat(dir);
4312
+ } catch (err) {
4313
+ return c.json({ error: describeFsError(err, dir) }, 400);
4314
+ }
4315
+ if (!stats.isDirectory()) {
4316
+ return c.json({ error: `path is not a directory: ${dir}` }, 400);
4317
+ }
4318
+ let rawEntries;
4319
+ try {
4320
+ rawEntries = await readdir(dir, { withFileTypes: true });
4321
+ } catch (err) {
4322
+ return c.json({ error: describeFsError(err, dir) }, 400);
4323
+ }
4324
+ const dirs = [];
4325
+ for (const entry of rawEntries) {
4326
+ if (!showHidden && entry.name.startsWith(".")) continue;
4327
+ const full = p.join(dir, entry.name);
4328
+ if (entry.isDirectory()) {
4329
+ dirs.push({ name: entry.name, path: full });
4330
+ continue;
4331
+ }
4332
+ if (!entry.isSymbolicLink()) continue;
4333
+ try {
4334
+ const s = await stat(full);
4335
+ if (s.isDirectory()) dirs.push({ name: entry.name, path: full });
4336
+ } catch {
4337
+ }
4338
+ }
4339
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
4340
+ const truncated = dirs.length > MAX_ENTRIES;
4341
+ return c.json({
4342
+ path: dir,
4343
+ parent: resolveParent(dir, platform2),
4344
+ sep: p.sep,
4345
+ home,
4346
+ entries: truncated ? dirs.slice(0, MAX_ENTRIES) : dirs,
4347
+ truncated
4348
+ });
4349
+ });
4350
+ app.post("/fs/mkdir", async (c) => {
4351
+ let body;
4352
+ try {
4353
+ body = await c.req.json();
4354
+ } catch {
4355
+ return c.json({ error: "invalid json body" }, 400);
4356
+ }
4357
+ const rawParent = typeof body.parent === "string" ? body.parent : "";
4358
+ const rawName = typeof body.name === "string" ? body.name : "";
4359
+ if (!rawParent.trim() || !p.isAbsolute(rawParent.trim())) {
4360
+ return c.json({ error: "parent must be an absolute directory path" }, 400);
4361
+ }
4362
+ const nameError = validateDirName(rawName, platform2);
4363
+ if (nameError) return c.json({ error: nameError }, 400);
4364
+ const parent = normalizeAbsPath(rawParent, platform2);
4365
+ try {
4366
+ const stats = await stat(parent);
4367
+ if (!stats.isDirectory()) {
4368
+ return c.json({ error: `path is not a directory: ${parent}` }, 400);
4369
+ }
4370
+ } catch (err) {
4371
+ return c.json({ error: describeFsError(err, parent) }, 400);
4372
+ }
4373
+ const target = p.join(parent, rawName.trim());
4374
+ try {
4375
+ await mkdir(target);
4376
+ } catch (err) {
4377
+ const code = err?.code;
4378
+ if (code === "EEXIST") return c.json({ error: `already exists: ${target}` }, 409);
4379
+ if (code === "EACCES" || code === "EPERM") {
4380
+ return c.json({ error: `permission denied: ${target}` }, 403);
4381
+ }
4382
+ return c.json({ error: describeFsError(err, target) }, 400);
4383
+ }
4384
+ return c.json({ path: normalizeAbsPath(target, platform2) }, 201);
4385
+ });
4386
+ return app;
4387
+ }
4388
+ const WIN_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i;
4389
+ function validateDirName(name2, platform2) {
4390
+ const value = name2.trim();
4391
+ if (!value) return "name must not be empty";
4392
+ if (value.length > 255) return "name is too long";
4393
+ if (value === "." || value === "..") return 'name must not be "." or ".."';
4394
+ if (value.includes("/") || value.includes("\\")) {
4395
+ return "name must not contain a path separator";
4396
+ }
4397
+ if (/[\u0000-\u001f\u007f]/.test(value)) return "name must not contain control characters";
4398
+ if (platform2 === "win32") {
4399
+ if (/[<>:"|?*]/.test(value)) return 'name must not contain <>:"|?* on Windows';
4400
+ if (WIN_RESERVED_NAME.test(value)) return `"${value}" is a reserved name on Windows`;
4401
+ if (/[. ]$/.test(value)) return "name must not end with a dot or space on Windows";
4402
+ }
4403
+ return null;
4404
+ }
4405
+ function resolveParent(dir, platform2) {
4406
+ if (platform2 !== "win32") {
4407
+ const parent2 = posix.dirname(dir);
4408
+ return parent2 === dir ? null : parent2;
4409
+ }
4410
+ const root = win32.parse(dir).root;
4411
+ if (dir === root) {
4412
+ return isUncRoot(root) ? null : WIN_DRIVE_ROOT;
4413
+ }
4414
+ const parent = win32.dirname(dir);
4415
+ return parent === dir ? null : parent;
4416
+ }
4417
+ function isUncRoot(root) {
4418
+ return root.startsWith("\\\\");
4419
+ }
4420
+ async function listWindowsDrives() {
4421
+ const letters = [];
4422
+ for (let code = "C".charCodeAt(0); code <= "Z".charCodeAt(0); code++) {
4423
+ letters.push(String.fromCharCode(code));
4424
+ }
4425
+ const checked = await Promise.all(
4426
+ letters.map(async (letter) => {
4427
+ const root = `${letter}:\\`;
4428
+ try {
4429
+ await access(root, constants.R_OK);
4430
+ return { name: `${letter}:`, path: root };
4431
+ } catch {
4432
+ return null;
4433
+ }
4434
+ })
4435
+ );
4436
+ return checked.filter((d) => d !== null);
4437
+ }
4438
+ function describeFsError(err, dir) {
4439
+ const code = err?.code;
4440
+ if (code === "ENOENT") return `path does not exist: ${dir}`;
4441
+ if (code === "ENAMETOOLONG") return `path is too long: ${dir}`;
4442
+ if (code === "EACCES" || code === "EPERM") return `permission denied: ${dir}`;
4443
+ if (code === "ENOTDIR") return `path is not a directory: ${dir}`;
4444
+ return `cannot read directory: ${dir}`;
4445
+ }
4241
4446
  class CommandRunStore {
4242
4447
  file;
4243
4448
  cache = null;
@@ -5784,6 +5989,7 @@ function createApp(opts) {
5784
5989
  api.route("/", createSpecDraftsRoutes(resolveProject));
5785
5990
  api.route("/", createWorktreeRoutes(opts.registry, worktreeManager));
5786
5991
  api.route("/", createProjectFilesRoutes(resolveProject));
5992
+ api.route("/", createFsRoutes());
5787
5993
  api.route("/", createCommandsRoutes(resolveProject));
5788
5994
  api.route(
5789
5995
  "/",
@@ -6368,13 +6574,57 @@ class SpecWatcher {
6368
6574
  return parts[0];
6369
6575
  }
6370
6576
  }
6577
+ function isRecord$2(value) {
6578
+ return !!value && typeof value === "object" && !Array.isArray(value);
6579
+ }
6580
+ const SPEC_WRITE_TOOLS = {
6581
+ claude: /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]),
6582
+ codex: /* @__PURE__ */ new Set(),
6583
+ opencode: /* @__PURE__ */ new Set(["write", "edit", "patch"]),
6584
+ pi: /* @__PURE__ */ new Set(["write", "edit"])
6585
+ };
6586
+ const PATH_KEYS = ["file_path", "filePath", "path"];
6587
+ function isSpecWrite(kind, name2, input) {
6588
+ if (typeof name2 !== "string" || !SPEC_WRITE_TOOLS[kind].has(name2)) return false;
6589
+ if (!isRecord$2(input)) return false;
6590
+ for (const key of PATH_KEYS) {
6591
+ const path = input[key];
6592
+ if (typeof path === "string" && path.endsWith("spec.md")) return true;
6593
+ }
6594
+ return false;
6595
+ }
6596
+ class PhaseAccumulator {
6597
+ constructor(kind, startedAt) {
6598
+ this.kind = kind;
6599
+ this.startedAt = startedAt;
6600
+ }
6601
+ usage = {};
6602
+ requests = 0;
6603
+ /** Count one API response, whether or not it reported usage. */
6604
+ add(raw) {
6605
+ this.requests += 1;
6606
+ const snapshot = normalizeUsage(this.kind, raw);
6607
+ if (!snapshot) return;
6608
+ for (const [key, value] of Object.entries(snapshot)) {
6609
+ const field = key;
6610
+ this.usage[field] = (this.usage[field] ?? 0) + value;
6611
+ }
6612
+ }
6613
+ snapshot() {
6614
+ return {
6615
+ usage: { ...this.usage },
6616
+ requests: this.requests,
6617
+ durationMs: Date.now() - this.startedAt
6618
+ };
6619
+ }
6620
+ }
6371
6621
  function blocksFrom(message) {
6372
6622
  if (!message || typeof message !== "object") return [];
6373
6623
  const content = message.content;
6374
6624
  if (typeof content === "string") return content ? [{ type: "text", text: content }] : [];
6375
6625
  return Array.isArray(content) ? content : [];
6376
6626
  }
6377
- function toolResultText(content) {
6627
+ function toolResultText$1(content) {
6378
6628
  if (typeof content === "string") return content;
6379
6629
  if (Array.isArray(content)) {
6380
6630
  let acc = "";
@@ -6420,10 +6670,10 @@ function usageWindows(usage) {
6420
6670
  }
6421
6671
  return out;
6422
6672
  }
6423
- function num$1(value) {
6673
+ function num$2(value) {
6424
6674
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
6425
6675
  }
6426
- function isRecord(value) {
6676
+ function isRecord$1(value) {
6427
6677
  return !!value && typeof value === "object" && !Array.isArray(value);
6428
6678
  }
6429
6679
  function withCost(usage, costUsd) {
@@ -6431,11 +6681,11 @@ function withCost(usage, costUsd) {
6431
6681
  return { ...usage ?? {}, costUsd };
6432
6682
  }
6433
6683
  function primaryModel(modelUsage) {
6434
- if (!isRecord(modelUsage)) return void 0;
6684
+ if (!isRecord$1(modelUsage)) return void 0;
6435
6685
  let best;
6436
6686
  let bestOutput = -1;
6437
6687
  for (const [model, raw] of Object.entries(modelUsage)) {
6438
- const output = isRecord(raw) ? num$1(raw.outputTokens) ?? 0 : 0;
6688
+ const output = isRecord$1(raw) ? num$2(raw.outputTokens) ?? 0 : 0;
6439
6689
  if (output > bestOutput) {
6440
6690
  bestOutput = output;
6441
6691
  best = model;
@@ -6443,38 +6693,8 @@ function primaryModel(modelUsage) {
6443
6693
  }
6444
6694
  return best;
6445
6695
  }
6446
- const SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
6447
- function isSpecWrite(name2, input) {
6448
- if (typeof name2 !== "string" || !SPEC_WRITE_TOOLS.has(name2)) return false;
6449
- const path = isRecord(input) ? input.file_path : void 0;
6450
- return typeof path === "string" && path.endsWith("spec.md");
6451
- }
6452
6696
  function usageOf(message) {
6453
- return isRecord(message) ? message.usage : void 0;
6454
- }
6455
- class PhaseAccumulator {
6456
- constructor(startedAt) {
6457
- this.startedAt = startedAt;
6458
- }
6459
- usage = {};
6460
- requests = 0;
6461
- /** Count one API response, whether or not it reported usage. */
6462
- add(raw) {
6463
- this.requests += 1;
6464
- const snapshot = normalizeUsage("claude", raw);
6465
- if (!snapshot) return;
6466
- for (const [key, value] of Object.entries(snapshot)) {
6467
- const field = key;
6468
- this.usage[field] = (this.usage[field] ?? 0) + value;
6469
- }
6470
- }
6471
- snapshot() {
6472
- return {
6473
- usage: { ...this.usage },
6474
- requests: this.requests,
6475
- durationMs: Date.now() - this.startedAt
6476
- };
6477
- }
6697
+ return isRecord$1(message) ? message.usage : void 0;
6478
6698
  }
6479
6699
  class ClaudeSession {
6480
6700
  constructor(id, cwd, isNew) {
@@ -6499,7 +6719,7 @@ class ClaudeSession {
6499
6719
  };
6500
6720
  if (this.isNew && !this.started) options.sessionId = this.id;
6501
6721
  else options.resume = this.id;
6502
- const phase = new PhaseAccumulator(Date.now());
6722
+ const phase = new PhaseAccumulator("claude", Date.now());
6503
6723
  let planPhase;
6504
6724
  const withPhases = (base) => ({
6505
6725
  ...base ?? {},
@@ -6524,26 +6744,26 @@ class ClaudeSession {
6524
6744
  if (b.type === "text" && typeof b.text === "string") {
6525
6745
  yield { type: "text", delta: b.text };
6526
6746
  } else if (b.type === "tool_use") {
6527
- if (!planPhase && isSpecWrite(b.name, b.input)) planPhase = phase.snapshot();
6747
+ if (!planPhase && isSpecWrite("claude", b.name, b.input)) planPhase = phase.snapshot();
6528
6748
  yield { type: "tool-use", name: String(b.name ?? "?"), input: b.input ?? {} };
6529
6749
  }
6530
6750
  }
6531
6751
  } else if (m.type === "user") {
6532
6752
  for (const b of blocksFrom(m.message)) {
6533
6753
  if (b.type === "tool_result") {
6534
- yield { type: "tool-result", text: toolResultText(b.content) };
6754
+ yield { type: "tool-result", text: toolResultText$1(b.content) };
6535
6755
  }
6536
6756
  }
6537
6757
  } else if (m.type === "result") {
6538
6758
  usage = m.usage;
6539
6759
  metrics = {
6540
- usage: withCost(normalizeUsage("claude", m.usage), num$1(m.total_cost_usd)),
6541
- modelUsage: isRecord(m.modelUsage) ? m.modelUsage : void 0,
6542
- numTurns: num$1(m.num_turns),
6760
+ usage: withCost(normalizeUsage("claude", m.usage), num$2(m.total_cost_usd)),
6761
+ modelUsage: isRecord$1(m.modelUsage) ? m.modelUsage : void 0,
6762
+ numTurns: num$2(m.num_turns),
6543
6763
  stopReason: typeof m.stop_reason === "string" ? m.stop_reason : void 0,
6544
6764
  model: primaryModel(m.modelUsage),
6545
- durationMs: num$1(m.duration_ms),
6546
- apiDurationMs: num$1(m.duration_api_ms)
6765
+ durationMs: num$2(m.duration_ms),
6766
+ apiDurationMs: num$2(m.duration_api_ms)
6547
6767
  };
6548
6768
  } else if (m.type === "system" && m.subtype === "compact_boundary") {
6549
6769
  const meta = m.compact_metadata ?? {};
@@ -6551,9 +6771,9 @@ class ClaudeSession {
6551
6771
  type: "compact",
6552
6772
  metrics: {
6553
6773
  trigger: meta.trigger === "manual" || meta.trigger === "auto" ? meta.trigger : void 0,
6554
- preTokens: num$1(meta.pre_tokens),
6555
- postTokens: num$1(meta.post_tokens),
6556
- durationMs: num$1(meta.duration_ms)
6774
+ preTokens: num$2(meta.pre_tokens),
6775
+ postTokens: num$2(meta.post_tokens),
6776
+ durationMs: num$2(meta.duration_ms)
6557
6777
  }
6558
6778
  };
6559
6779
  } else if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
@@ -6607,7 +6827,7 @@ class ClaudeAdapter {
6607
6827
  } else if (b.type === "tool_use") {
6608
6828
  parts.push({ type: "tool-use", name: String(b.name ?? "?"), input: b.input ?? {} });
6609
6829
  } else if (b.type === "tool_result") {
6610
- parts.push({ type: "tool-result", text: toolResultText(b.content) });
6830
+ parts.push({ type: "tool-result", text: toolResultText$1(b.content) });
6611
6831
  }
6612
6832
  }
6613
6833
  if (parts.length) out.push({ role: entry.type, parts });
@@ -6705,10 +6925,10 @@ function parseCodexSessionIndex(text) {
6705
6925
  }
6706
6926
  return out;
6707
6927
  }
6708
- function asRecord$1(value) {
6928
+ function asRecord(value) {
6709
6929
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
6710
6930
  }
6711
- function numberFrom$1(value) {
6931
+ function numberFrom(value) {
6712
6932
  if (typeof value === "number" && Number.isFinite(value)) return value;
6713
6933
  if (typeof value === "string" && value.trim()) {
6714
6934
  const n = Number(value);
@@ -6716,33 +6936,33 @@ function numberFrom$1(value) {
6716
6936
  }
6717
6937
  return null;
6718
6938
  }
6719
- function resetFrom$1(value) {
6939
+ function resetFrom(value) {
6720
6940
  if (typeof value === "string" && value.trim()) {
6721
6941
  const n2 = Number(value);
6722
6942
  if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
6723
6943
  const d = new Date(value);
6724
6944
  return Number.isFinite(d.getTime()) ? d.toISOString() : null;
6725
6945
  }
6726
- const n = numberFrom$1(value);
6946
+ const n = numberFrom(value);
6727
6947
  return n == null ? null : new Date(n * 1e3).toISOString();
6728
6948
  }
6729
6949
  function codexWindow(key, label, value) {
6730
- const rec = asRecord$1(value);
6950
+ const rec = asRecord(value);
6731
6951
  if (!rec) return null;
6732
- const utilization = numberFrom$1(rec.used_percent) ?? numberFrom$1(rec.usedPercent) ?? numberFrom$1(rec.utilization) ?? numberFrom$1(rec.percent_used);
6952
+ const utilization = numberFrom(rec.used_percent) ?? numberFrom(rec.usedPercent) ?? numberFrom(rec.utilization) ?? numberFrom(rec.percent_used);
6733
6953
  if (utilization == null) return null;
6734
6954
  return {
6735
6955
  key,
6736
6956
  label,
6737
6957
  utilization,
6738
- resetsAt: resetFrom$1(rec.resets_at ?? rec.reset_at ?? rec.resetsAt ?? rec.resetAt)
6958
+ resetsAt: resetFrom(rec.resets_at ?? rec.reset_at ?? rec.resetsAt ?? rec.resetAt)
6739
6959
  };
6740
6960
  }
6741
6961
  function parseCodexUsageResponse(raw) {
6742
- const root = asRecord$1(raw);
6962
+ const root = asRecord(raw);
6743
6963
  if (!root) return null;
6744
- const rateLimit = asRecord$1(root.rate_limit);
6745
- const rateLimits = asRecord$1(root.rate_limits);
6964
+ const rateLimit = asRecord(root.rate_limit);
6965
+ const rateLimits = asRecord(root.rate_limits);
6746
6966
  const windows = [];
6747
6967
  const primary = codexWindow(
6748
6968
  "primary",
@@ -6759,7 +6979,7 @@ function parseCodexUsageResponse(raw) {
6759
6979
  const additional = root.additional_rate_limits ?? root.additionalRateLimits ?? rateLimit?.additional;
6760
6980
  if (Array.isArray(additional)) {
6761
6981
  for (const [idx, item] of additional.entries()) {
6762
- const rec = asRecord$1(item);
6982
+ const rec = asRecord(item);
6763
6983
  const label = String(rec?.label ?? rec?.name ?? rec?.limit_name ?? `Additional ${idx + 1}`);
6764
6984
  const win = codexWindow(`additional:${idx}`, label, item);
6765
6985
  if (win) windows.push(win);
@@ -6788,8 +7008,8 @@ function parseCodexTokenCountSnapshot(text) {
6788
7008
  } catch {
6789
7009
  continue;
6790
7010
  }
6791
- const rec = asRecord$1(obj);
6792
- const payload = asRecord$1(rec?.payload);
7011
+ const rec = asRecord(obj);
7012
+ const payload = asRecord(rec?.payload);
6793
7013
  if (payload?.type !== "token_count") continue;
6794
7014
  const parsed = parseCodexUsageResponse({ rate_limits: payload.rate_limits });
6795
7015
  if (!parsed) continue;
@@ -6966,7 +7186,7 @@ class CodexAdapter {
6966
7186
  } catch {
6967
7187
  return null;
6968
7188
  }
6969
- const tokens = asRecord$1(asRecord$1(raw)?.tokens);
7189
+ const tokens = asRecord(asRecord(raw)?.tokens);
6970
7190
  const accessToken = typeof tokens?.access_token === "string" ? tokens.access_token : "";
6971
7191
  const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : "";
6972
7192
  return accessToken && accountId ? { accessToken, accountId } : null;
@@ -7065,136 +7285,6 @@ class CodexAdapter {
7065
7285
  return null;
7066
7286
  }
7067
7287
  }
7068
- const OPENCODE_QUOTA_INSTALL = "npx @slkiser/opencode-quota@latest init";
7069
- const OPENCODE_QUOTA_PLUGIN_BIN = join(
7070
- homedir(),
7071
- ".cache",
7072
- "opencode",
7073
- "packages",
7074
- "@slkiser",
7075
- "opencode-quota@latest",
7076
- "node_modules",
7077
- "@slkiser",
7078
- "opencode-quota",
7079
- "dist",
7080
- "bin",
7081
- "opencode-quota.js"
7082
- );
7083
- function asRecord(value) {
7084
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
7085
- }
7086
- function numberFrom(value) {
7087
- if (typeof value === "number" && Number.isFinite(value)) return value;
7088
- if (typeof value === "string" && value.trim()) {
7089
- const n = Number(value);
7090
- if (Number.isFinite(n)) return n;
7091
- }
7092
- return null;
7093
- }
7094
- function resetFrom(value) {
7095
- if (typeof value === "string" && value.trim()) {
7096
- const n2 = Number(value);
7097
- if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
7098
- const d = new Date(value);
7099
- return Number.isFinite(d.getTime()) ? d.toISOString() : null;
7100
- }
7101
- const n = numberFrom(value);
7102
- return n == null ? null : new Date(n * 1e3).toISOString();
7103
- }
7104
- function usageWindowFromRecord(rec, idx) {
7105
- const label = String(rec.label ?? rec.name ?? rec.title ?? rec.provider ?? `Window ${idx + 1}`);
7106
- const used = numberFrom(rec.usedPercent) ?? numberFrom(rec.used_percent) ?? numberFrom(rec.percent) ?? numberFrom(rec.utilization);
7107
- const remaining = numberFrom(rec.remainingPercent) ?? numberFrom(rec.remaining_percent) ?? numberFrom(rec.percentRemaining);
7108
- const utilization = used ?? (remaining == null ? null : 100 - remaining);
7109
- if (utilization == null) return null;
7110
- return {
7111
- key: String(rec.key ?? rec.id ?? label),
7112
- label,
7113
- utilization,
7114
- resetsAt: resetFrom(rec.resetsAt ?? rec.resets_at ?? rec.resetAt ?? rec.reset_at ?? rec.reset)
7115
- };
7116
- }
7117
- function collectOpencodeQuotaWindows(value, out, seen) {
7118
- if (!value || seen.has(value)) return;
7119
- seen.add(value);
7120
- if (Array.isArray(value)) {
7121
- for (const item of value) collectOpencodeQuotaWindows(item, out, seen);
7122
- return;
7123
- }
7124
- const rec = asRecord(value);
7125
- if (!rec) return;
7126
- const win = usageWindowFromRecord(rec, out.length);
7127
- if (win) out.push(win);
7128
- for (const key of ["windows", "quotas", "items", "limits", "entries"]) {
7129
- if (key in rec) collectOpencodeQuotaWindows(rec[key], out, seen);
7130
- }
7131
- for (const key of ["providers", "data"]) {
7132
- const child = asRecord(rec[key]);
7133
- if (child) {
7134
- for (const value2 of Object.values(child)) collectOpencodeQuotaWindows(value2, out, seen);
7135
- } else if (key in rec) {
7136
- collectOpencodeQuotaWindows(rec[key], out, seen);
7137
- }
7138
- }
7139
- }
7140
- function parseOpencodeQuotaOutput(text) {
7141
- let raw;
7142
- try {
7143
- raw = JSON.parse(text);
7144
- } catch {
7145
- return parseOpencodeQuotaText(text);
7146
- }
7147
- const windows = [];
7148
- collectOpencodeQuotaWindows(raw, windows, /* @__PURE__ */ new Set());
7149
- if (windows.length === 0) return null;
7150
- return {
7151
- kind: "opencode",
7152
- status: "available",
7153
- checkedAt: Date.now(),
7154
- source: "external-cli",
7155
- rateLimitsAvailable: true,
7156
- windows
7157
- };
7158
- }
7159
- function parseOpencodeQuotaText(text) {
7160
- const windows = [];
7161
- let provider = "";
7162
- for (const line of text.split("\n")) {
7163
- const providerMatch = line.match(/^\s*→\s*\[(.+?)]/);
7164
- if (providerMatch) {
7165
- provider = providerMatch[1].trim();
7166
- continue;
7167
- }
7168
- const quotaMatch = line.match(/^\s*(.+?quota)\s+.*?(\d+(?:\.\d+)?)%\s+left/i);
7169
- if (!quotaMatch) continue;
7170
- const label = [provider, quotaMatch[1].trim()].filter(Boolean).join(": ");
7171
- const remaining = Number(quotaMatch[2]);
7172
- windows.push({
7173
- key: label || `quota:${windows.length}`,
7174
- label: label || `Quota ${windows.length + 1}`,
7175
- utilization: Math.max(0, Math.min(100, 100 - remaining)),
7176
- resetsAt: null
7177
- });
7178
- }
7179
- if (windows.length === 0) return null;
7180
- return {
7181
- kind: "opencode",
7182
- status: "available",
7183
- checkedAt: Date.now(),
7184
- source: "external-cli",
7185
- rateLimitsAvailable: true,
7186
- windows
7187
- };
7188
- }
7189
- async function opencodeQuotaCommands() {
7190
- const commands = [{ cmd: "opencode-quota", args: ["show", "--json"] }];
7191
- try {
7192
- await access(OPENCODE_QUOTA_PLUGIN_BIN);
7193
- commands.push({ cmd: process.execPath, args: [OPENCODE_QUOTA_PLUGIN_BIN, "show", "--json"] });
7194
- } catch {
7195
- }
7196
- return commands;
7197
- }
7198
7288
  function textParts(parts) {
7199
7289
  const out = [];
7200
7290
  for (const p of parts) {
@@ -7265,7 +7355,19 @@ class OpenCodeAdapter {
7265
7355
  booting = null;
7266
7356
  async ensure() {
7267
7357
  if (!this.booting) {
7268
- this.booting = createOpencode({ hostname: "127.0.0.1" }).catch((err) => {
7358
+ this.booting = createOpencode({
7359
+ hostname: "127.0.0.1",
7360
+ port: 0,
7361
+ config: {
7362
+ permission: {
7363
+ edit: "allow",
7364
+ bash: "allow",
7365
+ webfetch: "allow",
7366
+ doom_loop: "allow",
7367
+ external_directory: "allow"
7368
+ }
7369
+ }
7370
+ }).catch((err) => {
7269
7371
  this.booting = null;
7270
7372
  throw err;
7271
7373
  });
@@ -7314,62 +7416,404 @@ class OpenCodeAdapter {
7314
7416
  }
7315
7417
  return out;
7316
7418
  }
7419
+ /**
7420
+ * `usageStatus: false` —— opencode 自身没有配额查询接口,早先那条路径是去调
7421
+ * 第三方 opencode-quota 插件;实测装了也拿不到可用数据,只会在空态里留下一行
7422
+ * 「去装插件」的噪音。与其留一个必然失败的探测(每次还要 spawn 一个 10s 超时
7423
+ * 的子进程),不如明确声明不支持:前端据此什么都不显示。
7424
+ */
7317
7425
  capabilities() {
7318
- return { listSessions: true, getMessages: true, usageStatus: true };
7426
+ return { listSessions: true, getMessages: true, usageStatus: false };
7319
7427
  }
7320
- async getUsageStatus() {
7321
- let lastError;
7322
- for (const command of await opencodeQuotaCommands()) {
7323
- try {
7324
- const { stdout } = await execFileWithoutWindow(command.cmd, command.args, {
7325
- cwd: this.cwd,
7326
- encoding: "utf8",
7327
- timeout: 1e4,
7328
- maxBuffer: 1024 * 1024
7329
- });
7330
- const parsed = parseOpencodeQuotaOutput(stdout);
7331
- if (parsed) return parsed;
7332
- return {
7333
- kind: this.kind,
7334
- status: "error",
7335
- checkedAt: Date.now(),
7336
- source: "external-cli",
7337
- message: "opencode-quota returned no parseable quota windows"
7428
+ async dispose() {
7429
+ if (!this.booting) return;
7430
+ try {
7431
+ const { server } = await this.booting;
7432
+ server.close();
7433
+ } catch {
7434
+ }
7435
+ this.booting = null;
7436
+ }
7437
+ }
7438
+ function isRecord(value) {
7439
+ return !!value && typeof value === "object" && !Array.isArray(value);
7440
+ }
7441
+ function num$1(value) {
7442
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7443
+ }
7444
+ function contentText(content) {
7445
+ if (typeof content === "string") return content;
7446
+ if (!Array.isArray(content)) return "";
7447
+ let acc = "";
7448
+ for (const c of content) {
7449
+ if (isRecord(c) && c.type === "text" && typeof c.text === "string") acc += c.text;
7450
+ }
7451
+ return acc;
7452
+ }
7453
+ function toolResultText(result) {
7454
+ if (typeof result === "string") return result;
7455
+ if (Array.isArray(result)) return contentText(result);
7456
+ if (isRecord(result)) {
7457
+ const inner = contentText(result.content);
7458
+ if (inner) return inner;
7459
+ if (typeof result.output === "string") return result.output;
7460
+ }
7461
+ return result == null ? "" : JSON.stringify(result);
7462
+ }
7463
+ function addUsage(acc, raw) {
7464
+ if (!isRecord(raw)) return;
7465
+ for (const key of [
7466
+ "input",
7467
+ "output",
7468
+ "cacheRead",
7469
+ "cacheWrite",
7470
+ "cacheWrite1h",
7471
+ "reasoning",
7472
+ "totalTokens"
7473
+ ]) {
7474
+ const value = num$1(raw[key]);
7475
+ if (value === void 0) continue;
7476
+ acc[key] = (acc[key] ?? 0) + value;
7477
+ }
7478
+ const cost = raw.cost;
7479
+ if (!isRecord(cost)) return;
7480
+ const target = acc.cost ??= {};
7481
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
7482
+ const value = num$1(cost[key]);
7483
+ if (value === void 0) continue;
7484
+ target[key] = (target[key] ?? 0) + value;
7485
+ }
7486
+ }
7487
+ function hasUsage(usage) {
7488
+ return Object.keys(usage).length > 0;
7489
+ }
7490
+ class EventQueue {
7491
+ items = [];
7492
+ wake = null;
7493
+ closed = false;
7494
+ push(item) {
7495
+ this.items.push(item);
7496
+ this.notify();
7497
+ }
7498
+ /** Stop after the already-queued items drain — never discards them. */
7499
+ close() {
7500
+ this.closed = true;
7501
+ this.notify();
7502
+ }
7503
+ notify() {
7504
+ const resume = this.wake;
7505
+ this.wake = null;
7506
+ resume?.();
7507
+ }
7508
+ async *drain() {
7509
+ for (; ; ) {
7510
+ while (this.items.length) yield this.items.shift();
7511
+ if (this.closed) return;
7512
+ await new Promise((resolve2) => {
7513
+ this.wake = resolve2;
7514
+ });
7515
+ }
7516
+ }
7517
+ }
7518
+ class PiSession {
7519
+ constructor(id, cwd, isNew, adapter) {
7520
+ this.id = id;
7521
+ this.cwd = cwd;
7522
+ this.isNew = isNew;
7523
+ this.adapter = adapter;
7524
+ }
7525
+ started = false;
7526
+ live = null;
7527
+ aborted = false;
7528
+ /**
7529
+ * One Pi session per turn.
7530
+ *
7531
+ * `AgentSession` has no release hook, so a long-lived `PiAgentSession` would
7532
+ * leak its listeners and extension runtime for as long as the chat exists.
7533
+ * The expensive part — model catalog and auth — is hoisted into the adapter's
7534
+ * shared `ModelRuntime`, leaving per-turn setup cheap.
7535
+ */
7536
+ async *send(prompt, opts) {
7537
+ this.aborted = false;
7538
+ if (opts?.signal) {
7539
+ if (opts.signal.aborted) return;
7540
+ opts.signal.addEventListener("abort", () => this.abort(), { once: true });
7541
+ }
7542
+ const phase = new PhaseAccumulator("pi", Date.now());
7543
+ let planPhase;
7544
+ const usage = {};
7545
+ let model;
7546
+ let stopReason;
7547
+ let turns = 0;
7548
+ let compactionStartedAt = 0;
7549
+ const startedAt = Date.now();
7550
+ let session;
7551
+ let resumeError;
7552
+ try {
7553
+ const resolved = await this.openPiSession();
7554
+ session = resolved.session;
7555
+ resumeError = resolved.resumeError;
7556
+ } catch (err) {
7557
+ yield { type: "error", message: err instanceof Error ? err.message : String(err) };
7558
+ return;
7559
+ }
7560
+ this.live = session;
7561
+ if (resumeError) yield { type: "error", message: resumeError };
7562
+ if (!this.started || session.sessionId !== this.id) {
7563
+ this.started = true;
7564
+ this.id = session.sessionId;
7565
+ yield { type: "session-started", sessionId: this.id };
7566
+ }
7567
+ const queue = new EventQueue();
7568
+ const unsubscribe = session.subscribe((event) => queue.push(event));
7569
+ let promptError;
7570
+ const running = session.prompt(prompt, { expandPromptTemplates: false }).catch((err) => {
7571
+ promptError = err;
7572
+ }).finally(() => queue.close());
7573
+ try {
7574
+ for await (const event of queue.drain()) {
7575
+ switch (event.type) {
7576
+ case "message_update": {
7577
+ const delta = event.assistantMessageEvent;
7578
+ if (delta.type === "text_delta" && delta.delta) {
7579
+ yield { type: "text", delta: delta.delta };
7580
+ }
7581
+ break;
7582
+ }
7583
+ case "message_end": {
7584
+ const message = event.message;
7585
+ if (isRecord(message) && message.role === "assistant") {
7586
+ phase.add(message.usage);
7587
+ addUsage(usage, message.usage);
7588
+ if (typeof message.model === "string") model = message.model;
7589
+ if (typeof message.stopReason === "string") stopReason = message.stopReason;
7590
+ if (message.stopReason === "error") {
7591
+ const detail = typeof message.errorMessage === "string" && message.errorMessage ? message.errorMessage : "pi assistant turn failed";
7592
+ yield { type: "error", message: detail };
7593
+ }
7594
+ }
7595
+ break;
7596
+ }
7597
+ case "turn_end":
7598
+ turns += 1;
7599
+ break;
7600
+ case "tool_execution_start": {
7601
+ if (!planPhase && isSpecWrite("pi", event.toolName, event.args)) {
7602
+ planPhase = phase.snapshot();
7603
+ }
7604
+ yield { type: "tool-use", name: event.toolName, input: event.args ?? {} };
7605
+ break;
7606
+ }
7607
+ case "tool_execution_end": {
7608
+ const text = toolResultText(event.result);
7609
+ if (text) yield { type: "tool-result", text };
7610
+ break;
7611
+ }
7612
+ case "compaction_start":
7613
+ compactionStartedAt = Date.now();
7614
+ break;
7615
+ case "compaction_end": {
7616
+ if (event.aborted) break;
7617
+ if (event.errorMessage) {
7618
+ yield { type: "error", message: event.errorMessage };
7619
+ break;
7620
+ }
7621
+ yield {
7622
+ type: "compact",
7623
+ metrics: {
7624
+ trigger: event.reason === "manual" ? "manual" : "auto",
7625
+ preTokens: num$1(event.result?.tokensBefore),
7626
+ postTokens: num$1(event.result?.estimatedTokensAfter),
7627
+ durationMs: compactionStartedAt ? Date.now() - compactionStartedAt : void 0
7628
+ }
7629
+ };
7630
+ break;
7631
+ }
7632
+ default:
7633
+ break;
7634
+ }
7635
+ }
7636
+ await running;
7637
+ if (this.aborted) return;
7638
+ if (promptError) {
7639
+ yield {
7640
+ type: "error",
7641
+ message: promptError instanceof Error ? promptError.message : String(promptError)
7338
7642
  };
7339
- } catch (err2) {
7340
- lastError = err2;
7341
- if (err2.code === "ENOENT") continue;
7342
- break;
7643
+ return;
7343
7644
  }
7344
- }
7345
- const err = lastError;
7346
- const code = err?.code;
7347
- if (code === "ENOENT") {
7348
- return {
7349
- kind: this.kind,
7350
- status: "unavailable",
7351
- checkedAt: Date.now(),
7352
- source: "external-cli",
7353
- installCommand: OPENCODE_QUOTA_INSTALL,
7354
- message: "opencode-quota is not installed"
7645
+ const metrics = {
7646
+ usage: normalizeUsage("pi", usage),
7647
+ numTurns: turns || void 0,
7648
+ stopReason,
7649
+ model,
7650
+ durationMs: Date.now() - startedAt,
7651
+ planPhase,
7652
+ observedTotal: phase.snapshot()
7355
7653
  };
7654
+ yield { type: "turn-completed", usage: hasUsage(usage) ? usage : void 0, metrics };
7655
+ } finally {
7656
+ unsubscribe();
7657
+ session.dispose();
7658
+ this.live = null;
7659
+ this.started = true;
7356
7660
  }
7357
- return {
7661
+ }
7662
+ abort() {
7663
+ this.aborted = true;
7664
+ void this.live?.abort();
7665
+ }
7666
+ /** Build (or reopen) the Pi session backing this turn. */
7667
+ async openPiSession() {
7668
+ let resumeError;
7669
+ let manager;
7670
+ if (this.isNew && !this.started) {
7671
+ manager = SessionManager$1.create(this.cwd, this.adapter.sessionDirOption, { id: this.id });
7672
+ } else {
7673
+ const path = await this.adapter.resolveSessionPath(this.id);
7674
+ if (path) {
7675
+ manager = SessionManager$1.open(path, this.adapter.sessionDirOption, this.cwd);
7676
+ } else {
7677
+ resumeError = `pi session ${this.id} was not found on disk; started a new session instead`;
7678
+ manager = SessionManager$1.create(this.cwd, this.adapter.sessionDirOption);
7679
+ }
7680
+ }
7681
+ const { session } = await createAgentSession({
7682
+ cwd: this.cwd,
7683
+ agentDir: this.adapter.agentDirOption,
7684
+ modelRuntime: await this.adapter.ensureRuntime(),
7685
+ sessionManager: manager
7686
+ });
7687
+ return { session, resumeError };
7688
+ }
7689
+ }
7690
+ class PiAdapter {
7691
+ constructor(cwd, opts = {}) {
7692
+ this.cwd = cwd;
7693
+ this.opts = opts;
7694
+ }
7695
+ kind = "pi";
7696
+ runtime = null;
7697
+ /** `session id → JSONL path`, refreshed from disk on a miss. */
7698
+ pathIndex = /* @__PURE__ */ new Map();
7699
+ get agentDirOption() {
7700
+ return this.opts.agentDir;
7701
+ }
7702
+ get sessionDirOption() {
7703
+ return this.opts.sessionDir;
7704
+ }
7705
+ /**
7706
+ * Lazily build one `ModelRuntime` for every session this adapter serves.
7707
+ *
7708
+ * Creating it reads auth and the model catalog — the dominant cost of
7709
+ * `createAgentSession()`. On failure the memo is cleared so the next turn
7710
+ * retries instead of replaying a cached rejection forever.
7711
+ */
7712
+ async ensureRuntime() {
7713
+ if (!this.runtime) {
7714
+ const agentDir = this.opts.agentDir;
7715
+ this.runtime = ModelRuntime.create(
7716
+ agentDir ? { authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") } : void 0
7717
+ ).catch((err) => {
7718
+ this.runtime = null;
7719
+ throw err;
7720
+ });
7721
+ }
7722
+ return this.runtime;
7723
+ }
7724
+ async createSession() {
7725
+ return new PiSession(randomUUID(), this.cwd, true, this);
7726
+ }
7727
+ async resumeSession(id) {
7728
+ return new PiSession(id, this.cwd, false, this);
7729
+ }
7730
+ async listSessions() {
7731
+ const infos = await this.listPiSessions();
7732
+ return infos.map((info) => ({
7733
+ id: info.id,
7734
+ title: this.titleOf(info),
7358
7735
  kind: this.kind,
7359
- status: "error",
7360
- checkedAt: Date.now(),
7361
- source: "external-cli",
7362
- message: err instanceof Error ? err.message : String(err)
7363
- };
7736
+ createdAt: info.created.getTime(),
7737
+ updatedAt: info.modified.getTime()
7738
+ })).sort((a, b) => b.updatedAt - a.updatedAt);
7739
+ }
7740
+ async getMessages(id) {
7741
+ const path = await this.resolveSessionPath(id);
7742
+ if (!path) return [];
7743
+ let entries;
7744
+ try {
7745
+ entries = SessionManager$1.open(path, this.opts.sessionDir, this.cwd).getEntries();
7746
+ } catch {
7747
+ return [];
7748
+ }
7749
+ const out = [];
7750
+ for (const entry of entries) {
7751
+ if (entry.type !== "message") continue;
7752
+ const message = entry.message;
7753
+ if (!isRecord(message)) continue;
7754
+ const ts = num$1(message.timestamp);
7755
+ if (message.role === "user") {
7756
+ const text = contentText(message.content);
7757
+ if (text) out.push({ role: "user", parts: [{ type: "text", text }], ts });
7758
+ } else if (message.role === "assistant") {
7759
+ const parts = [];
7760
+ const content = Array.isArray(message.content) ? message.content : [];
7761
+ for (const block of content) {
7762
+ if (!isRecord(block)) continue;
7763
+ if (block.type === "text" && typeof block.text === "string" && block.text) {
7764
+ parts.push({ type: "text", text: block.text });
7765
+ } else if (block.type === "toolCall") {
7766
+ parts.push({
7767
+ type: "tool-use",
7768
+ name: String(block.name ?? "?"),
7769
+ input: block.arguments ?? {}
7770
+ });
7771
+ }
7772
+ }
7773
+ if (parts.length) out.push({ role: "assistant", parts, ts });
7774
+ } else if (message.role === "toolResult") {
7775
+ const text = toolResultText(message.content);
7776
+ if (!text) continue;
7777
+ const last = out[out.length - 1];
7778
+ if (last?.role === "assistant") last.parts.push({ type: "tool-result", text });
7779
+ }
7780
+ }
7781
+ return out;
7782
+ }
7783
+ /**
7784
+ * `usageStatus: false` — Pi is BYO-provider: `ModelRuntime` knows whether auth
7785
+ * is configured but has no quota window to report. Declaring it unsupported
7786
+ * lets the GUI render nothing, which beats a probe that must always fail.
7787
+ */
7788
+ capabilities() {
7789
+ return { listSessions: true, getMessages: true, usageStatus: false };
7364
7790
  }
7365
7791
  async dispose() {
7366
- if (!this.booting) return;
7792
+ this.runtime = null;
7793
+ this.pathIndex.clear();
7794
+ }
7795
+ /** Resolve a session id to its JSONL path, refreshing the index on a miss. */
7796
+ async resolveSessionPath(id) {
7797
+ const cached = this.pathIndex.get(id);
7798
+ if (cached) return cached;
7799
+ await this.listPiSessions();
7800
+ return this.pathIndex.get(id) ?? null;
7801
+ }
7802
+ async listPiSessions() {
7803
+ let infos;
7367
7804
  try {
7368
- const { server } = await this.booting;
7369
- server.close();
7805
+ infos = await SessionManager$1.list(this.cwd, this.opts.sessionDir);
7370
7806
  } catch {
7807
+ return [];
7371
7808
  }
7372
- this.booting = null;
7809
+ const mine = infos.filter((info) => !info.cwd || info.cwd === this.cwd);
7810
+ for (const info of mine) this.pathIndex.set(info.id, info.path);
7811
+ return mine;
7812
+ }
7813
+ titleOf(info) {
7814
+ if (info.name?.trim()) return info.name.trim();
7815
+ const summarized = info.firstMessage ? summarizeCodexPromptForTitle(info.firstMessage) : "";
7816
+ return summarized || info.id;
7373
7817
  }
7374
7818
  }
7375
7819
  function createAdapter(kind, cwd) {
@@ -7378,6 +7822,8 @@ function createAdapter(kind, cwd) {
7378
7822
  return new CodexAdapter(cwd);
7379
7823
  case "opencode":
7380
7824
  return new OpenCodeAdapter(cwd);
7825
+ case "pi":
7826
+ return new PiAdapter(cwd);
7381
7827
  case "claude":
7382
7828
  default:
7383
7829
  return new ClaudeAdapter(cwd);
@@ -7519,7 +7965,7 @@ class SessionManager {
7519
7965
  const indexed = await this.store.list();
7520
7966
  const byId = new Map(indexed.map((s) => [s.id, s]));
7521
7967
  const nativeIds = /* @__PURE__ */ new Set();
7522
- for (const kind of ["claude", "codex", "opencode"]) {
7968
+ for (const kind of ["claude", "codex", "opencode", "pi"]) {
7523
7969
  const adapter = this.adapters.get(kind);
7524
7970
  if (!adapter.capabilities().listSessions) continue;
7525
7971
  try {
@@ -7958,6 +8404,22 @@ class ProjectRegistry {
7958
8404
  const normalized = await prepareProjectDir(absPath);
7959
8405
  return await addProject(normalized, this.globalConfigPath);
7960
8406
  }
8407
+ /**
8408
+ * 与 CLI `yorz add` 语义对齐的添加:含 git 仓库检查 / `git init` / `.gitignore` 写入。
8409
+ *
8410
+ * @param absPath 项目目录绝对路径(调用方需先归一化)。
8411
+ * @param opts `gitInit` 为 true 时授权对非 git 目录自动执行 `git init`。
8412
+ * @returns 注册结果。
8413
+ * @throws {NeedGitInitError} 目标非 git 仓库且未授权 `git init`;此时无任何副作用。
8414
+ */
8415
+ async addWithGit(absPath, opts = {}) {
8416
+ if (!isAbsolute(absPath)) throw new Error(`path must be absolute: ${absPath}`);
8417
+ return await addProjectWithGit({
8418
+ path: absPath,
8419
+ gitInit: opts.gitInit === true,
8420
+ ...this.globalConfigPath !== void 0 ? { globalConfigPath: this.globalConfigPath } : {}
8421
+ });
8422
+ }
7961
8423
  async remove(id) {
7962
8424
  await this.release(id).catch(() => {
7963
8425
  });
@@ -8068,7 +8530,9 @@ class ProjectRegistry {
8068
8530
  }
8069
8531
  function resolveProjectAgentKind(projectAgent, globalDefault) {
8070
8532
  if (projectAgent.kind === "inherit") return globalDefault;
8071
- if (projectAgent.kind === "codex" || projectAgent.kind === "opencode") return projectAgent.kind;
8533
+ if (projectAgent.kind === "codex" || projectAgent.kind === "opencode" || projectAgent.kind === "pi") {
8534
+ return projectAgent.kind;
8535
+ }
8072
8536
  return "claude";
8073
8537
  }
8074
8538
  function basename(p) {
@@ -8524,17 +8988,14 @@ function startBackgroundServe(opts) {
8524
8988
  const entry = process.argv[1];
8525
8989
  if (!entry) throw new Error("Cannot resolve CLI entrypoint for background service");
8526
8990
  const stdio = backgroundStdio();
8527
- const child = spawnWithoutWindow(process.execPath, [entry, "serve", ...backgroundArgs(opts)], {
8528
- detached: true,
8529
- stdio: stdio.stdio
8530
- });
8531
- child.unref();
8991
+ const child = spawnBackgroundWorker([entry, "serve", ...backgroundArgs(opts)], stdio.stdio);
8532
8992
  log().info("background service spawned", {
8533
8993
  pid: child.pid,
8994
+ viaLauncher: process.platform === "win32",
8534
8995
  stdioFile: stdio.path,
8535
8996
  logFile: getLogger().filePath
8536
8997
  });
8537
- const runtime = await waitForRuntime(child.pid, START_WAIT_MS);
8998
+ const runtime = await waitForRuntime(void 0, START_WAIT_MS);
8538
8999
  if (!runtime) {
8539
9000
  log().warn("timed out waiting for runtime.json", {
8540
9001
  pid: child.pid,
@@ -8543,12 +9004,13 @@ function startBackgroundServe(opts) {
8543
9004
  }
8544
9005
  const port = runtime?.port ?? opts.port ?? DEFAULT_SERVE_PORT;
8545
9006
  const url = runtime?.url ?? `http://localhost:${port}/`;
8546
- console.log(`YorZ Service started in background (pid=${child.pid ?? "unknown"}).`);
9007
+ const servicePid = runtime?.pid ?? child.pid;
9008
+ console.log(`YorZ Service started in background (pid=${servicePid ?? "unknown"}).`);
8547
9009
  console.log(`Open ${url}${runtime ? "" : ` (or the next free port if ${port} is busy).`}`);
8548
9010
  console.log(`Stop with: yorz serve stop`);
8549
9011
  return {
8550
9012
  background: true,
8551
- pid: child.pid,
9013
+ pid: servicePid,
8552
9014
  url,
8553
9015
  port
8554
9016
  };
@@ -8575,6 +9037,26 @@ function restartWorkerArgs(opts) {
8575
9037
  args.push("--skip-skill-check");
8576
9038
  return args;
8577
9039
  }
9040
+ function resolveServeLauncherPath() {
9041
+ return join(dirname(fileURLToPath(import.meta.url)), "serve-launcher.cjs");
9042
+ }
9043
+ function backgroundWorkerSpawnArgv(launcherPath, execPath, workerArgv) {
9044
+ return [launcherPath, "--", execPath, ...workerArgv];
9045
+ }
9046
+ function spawnBackgroundWorker(workerArgv, stdio) {
9047
+ if (process.platform === "win32") {
9048
+ const launcher = spawnWithoutWindow(
9049
+ process.execPath,
9050
+ backgroundWorkerSpawnArgv(resolveServeLauncherPath(), process.execPath, workerArgv),
9051
+ { detached: true, stdio }
9052
+ );
9053
+ launcher.unref();
9054
+ return launcher;
9055
+ }
9056
+ const child = spawnWithoutWindow(process.execPath, workerArgv, { detached: true, stdio });
9057
+ child.unref();
9058
+ return child;
9059
+ }
8578
9060
  async function runRestartServe(opts = {}) {
8579
9061
  silenceConsoleMirror();
8580
9062
  if (opts.worker) {
@@ -8585,12 +9067,8 @@ async function runRestartServe(opts = {}) {
8585
9067
  }
8586
9068
  const entry = process.argv[1];
8587
9069
  if (!entry) throw new Error("Cannot resolve CLI entrypoint for restart");
8588
- const child = spawnWithoutWindow(process.execPath, [entry, ...restartWorkerArgs(opts)], {
8589
- detached: true,
8590
- stdio: "ignore"
8591
- });
8592
- child.unref();
8593
- console.log(`YorZ Service restart scheduled (pid=${child.pid ?? "unknown"}).`);
9070
+ spawnBackgroundWorker([entry, ...restartWorkerArgs(opts)], "ignore");
9071
+ console.log(`YorZ Service restart scheduled.`);
8594
9072
  }
8595
9073
  async function ensureSkillsInstalledWithLog(cwd) {
8596
9074
  const results = await ensureSkillsInstalled({ cwd });
@@ -8762,7 +9240,7 @@ async function waitForRuntime(pid, timeoutMs) {
8762
9240
  const until = Date.now() + timeoutMs;
8763
9241
  while (Date.now() < until) {
8764
9242
  const live = await readLiveProcesses();
8765
- const match = live.find((p) => pid === void 0 || p.pid === pid);
9243
+ const match = live.find((p) => pid === void 0);
8766
9244
  if (match) return match;
8767
9245
  await sleep(100);
8768
9246
  }
@@ -8923,12 +9401,18 @@ async function readWindowsProcessSnapshot(pid) {
8923
9401
  "if ($null -eq $p) { exit 1 }",
8924
9402
  "$p | Select-Object CommandLine,CreationDate | ConvertTo-Json -Compress"
8925
9403
  ].join("; ");
8926
- const { stdout } = await execFileAsync("powershell.exe", [
8927
- "-NoProfile",
8928
- "-NonInteractive",
8929
- "-Command",
8930
- script
8931
- ]);
9404
+ const { stdout } = await execFileAsync(
9405
+ "powershell.exe",
9406
+ [
9407
+ "-NoProfile",
9408
+ "-NonInteractive",
9409
+ "-Command",
9410
+ script
9411
+ ],
9412
+ // 后台 Service 进程(detached,无控制台)内执行时,缺 windowsHide 会弹出
9413
+ // 可见的 PowerShell 窗口;前台终端调用则继承控制台,本就无影响。
9414
+ { windowsHide: true }
9415
+ );
8932
9416
  const parsed = JSON.parse(stdout.trim());
8933
9417
  const commandLine = typeof parsed.CommandLine === "string" ? parsed.CommandLine : "";
8934
9418
  const processStartedAt = typeof parsed.CreationDate === "string" ? parsed.CreationDate : void 0;
@@ -8964,35 +9448,33 @@ async function defaultPrompt(question) {
8964
9448
  }
8965
9449
  }
8966
9450
  async function runAdd(opts) {
8967
- const abs = await prepareProjectDir(opts.path, opts.cwd);
8968
- const gitInit = opts.runGitInit ?? runGitInit;
8969
- const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
8970
- let gitInitialized = false;
8971
- if (!await isGitRepo(abs)) {
8972
- if (opts.yes) {
8973
- await gitInit(abs);
8974
- gitInitialized = true;
8975
- } else if (isTTY) {
8976
- const ask = opts.prompt ?? defaultPrompt;
8977
- const raw = await ask(`yorz add: ${abs} 未 git init,是否自动执行 \`git init\`? [y/N] `);
8978
- const answer = raw.trim().toLowerCase();
8979
- if (answer === "y" || answer === "yes") {
8980
- await gitInit(abs);
8981
- gitInitialized = true;
8982
- } else {
9451
+ const gitInit = opts.runGitInit ?? ((cwd) => runGitInit(cwd));
9452
+ const base = {
9453
+ path: opts.path,
9454
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
9455
+ ...opts.globalConfigPath !== void 0 ? { globalConfigPath: opts.globalConfigPath } : {},
9456
+ runGitInit: gitInit
9457
+ };
9458
+ try {
9459
+ return await addProjectWithGit(base);
9460
+ } catch (err) {
9461
+ if (!(err instanceof NeedGitInitError)) throw err;
9462
+ const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
9463
+ if (!opts.yes) {
9464
+ if (!isTTY) {
8983
9465
  throw new AddGitAbortedError(
8984
- `yorz add: aborted — target directory is not a git repository`
9466
+ `yorz add: target directory is not a git repository; pass --yes to auto-run git init in non-interactive mode`
8985
9467
  );
8986
9468
  }
8987
- } else {
8988
- throw new AddGitAbortedError(
8989
- `yorz add: target directory is not a git repository; pass --yes to auto-run git init in non-interactive mode`
8990
- );
9469
+ const ask = opts.prompt ?? defaultPrompt;
9470
+ const raw = await ask(`yorz add: ${err.path} 未 git init,是否自动执行 \`git init\`? [y/N] `);
9471
+ const answer = raw.trim().toLowerCase();
9472
+ if (answer !== "y" && answer !== "yes") {
9473
+ throw new AddGitAbortedError(`yorz add: aborted — target directory is not a git repository`);
9474
+ }
8991
9475
  }
9476
+ return await addProjectWithGit({ ...base, gitInit: true });
8992
9477
  }
8993
- const gitignore = await ensureTmpIgnored(abs);
8994
- const { entry, created } = await addProject(abs, opts.globalConfigPath);
8995
- return { entry, created, gitInitialized, gitignore };
8996
9478
  }
8997
9479
  const md = new MarkdownIt("commonmark", { html: false, linkify: false });
8998
9480
  function buildContext(raw, kind, filePath) {