@carboncode/cli 0.1.0 → 0.1.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 (99) hide show
  1. package/README.md +15 -24
  2. package/README.zh-CN.md +13 -11
  3. package/dist/cli/{acp-35C4ME6Y.js → acp-E6QAGSG4.js} +17 -16
  4. package/dist/cli/acp-E6QAGSG4.js.map +1 -0
  5. package/dist/cli/{chat-A6UJDPGV.js → chat-6MBLB7MU.js} +21 -21
  6. package/dist/cli/{chunk-J5BYPUB5.js → chunk-2EKLWOE3.js} +794 -554
  7. package/dist/cli/chunk-2EKLWOE3.js.map +1 -0
  8. package/dist/cli/{chunk-UI66BH6D.js → chunk-2UXHZAXP.js} +2 -2
  9. package/dist/cli/{chunk-IX6XI2RG.js → chunk-3PNIUDTA.js} +2 -2
  10. package/dist/cli/{chunk-JKGYMRX5.js → chunk-676EW5HH.js} +2 -2
  11. package/dist/cli/{chunk-BSINVTTL.js → chunk-7R3PKZXB.js} +7 -7
  12. package/dist/cli/{chunk-3T6VBZCL.js → chunk-B2RA3NMS.js} +2 -2
  13. package/dist/cli/{chunk-CPKCNHRR.js → chunk-BYBOY5UY.js} +5 -5
  14. package/dist/cli/{chunk-3OAR6NVL.js → chunk-GG2V37EH.js} +2 -2
  15. package/dist/cli/{chunk-4IBIPQVB.js → chunk-J6UWUIT2.js} +3 -3
  16. package/dist/cli/{chunk-4TVNJWMA.js → chunk-LRXTF6NZ.js} +178 -18
  17. package/dist/cli/chunk-LRXTF6NZ.js.map +1 -0
  18. package/dist/cli/{chunk-T5TQ4NDT.js → chunk-MDQWQYBS.js} +3 -3
  19. package/dist/cli/{chunk-TH756VLN.js → chunk-NCMC7AMN.js} +240 -191
  20. package/dist/cli/chunk-NCMC7AMN.js.map +1 -0
  21. package/dist/cli/{chunk-ILJOIQ5W.js → chunk-NTF4IE7G.js} +2 -2
  22. package/dist/cli/{chunk-XJ5SRLKK.js → chunk-OYAIE6C7.js} +2 -2
  23. package/dist/cli/{chunk-BSGCXZQN.js → chunk-PT4UDK7Z.js} +2 -2
  24. package/dist/cli/{chunk-QJG7OF27.js → chunk-QLPHVU3W.js} +27 -10
  25. package/dist/cli/chunk-QLPHVU3W.js.map +1 -0
  26. package/dist/cli/{chunk-WRN65TRD.js → chunk-RLQQOIBS.js} +2 -2
  27. package/dist/cli/{chunk-QVC75MR3.js → chunk-U7RHC7D6.js} +2 -2
  28. package/dist/cli/{chunk-IAUOP25G.js → chunk-V6A26HU5.js} +34 -20
  29. package/dist/cli/chunk-V6A26HU5.js.map +1 -0
  30. package/dist/cli/{chunk-D5NFKRGO.js → chunk-VVQTSZWX.js} +2 -2
  31. package/dist/cli/{chunk-7L2WTRNU.js → chunk-X3IHKOYW.js} +2 -2
  32. package/dist/cli/{chunk-S2KIUQKQ.js → chunk-XJIF545V.js} +7 -6
  33. package/dist/cli/{chunk-S2KIUQKQ.js.map → chunk-XJIF545V.js.map} +1 -1
  34. package/dist/cli/{chunk-4MQ3VURH.js → chunk-ZADUQPQP.js} +24 -24
  35. package/dist/cli/chunk-ZADUQPQP.js.map +1 -0
  36. package/dist/cli/{code-4TUTAGO5.js → code-LBRSX6ZI.js} +24 -33
  37. package/dist/cli/code-LBRSX6ZI.js.map +1 -0
  38. package/dist/cli/{commands-KMOZEYCF.js → commands-PCHFC3CL.js} +4 -4
  39. package/dist/cli/{commit-DTFA56VQ.js → commit-HDN6VJBA.js} +3 -3
  40. package/dist/cli/{desktop-7N3MHNBD.js → desktop-RHWSCBHO.js} +17 -17
  41. package/dist/cli/{diff-E5OWTF4C.js → diff-MV5JNUH4.js} +8 -8
  42. package/dist/cli/{doctor-IEJQRJMN.js → doctor-QLO4V4DD.js} +8 -8
  43. package/dist/cli/index.js +32 -32
  44. package/dist/cli/{mcp-PDI2PDLG.js → mcp-JSHFAINM.js} +2 -2
  45. package/dist/cli/{mcp-browse-OSPXOFPZ.js → mcp-browse-ESMKKKYH.js} +2 -2
  46. package/dist/cli/{mcp-inspect-QRFVTHMF.js → mcp-inspect-WSUN36FM.js} +2 -2
  47. package/dist/cli/{prompt-3CDII3UO.js → prompt-5LMDCF4M.js} +3 -3
  48. package/dist/cli/{replay-HYOSRQIV.js → replay-D3ILR2YO.js} +8 -8
  49. package/dist/cli/{run-2ZHADOUP.js → run-6NN3P5JM.js} +13 -13
  50. package/dist/cli/{server-X75PAZG5.js → server-HE7LFAHH.js} +10 -10
  51. package/dist/cli/{sessions-POOZA5CQ.js → sessions-HXDBQM3V.js} +12 -12
  52. package/dist/cli/{setup-YLPFI3OH.js → setup-EP3UPG3F.js} +5 -5
  53. package/dist/cli/{stats-NXJ3TO2D.js → stats-5RC6P5TN.js} +6 -6
  54. package/dist/cli/{version-NXXWE3WN.js → version-AIR25TRN.js} +12 -12
  55. package/dist/index.d.ts +15 -2
  56. package/dist/index.js +328 -89
  57. package/dist/index.js.map +1 -1
  58. package/package.json +2 -2
  59. package/dist/cli/acp-35C4ME6Y.js.map +0 -1
  60. package/dist/cli/chunk-4MQ3VURH.js.map +0 -1
  61. package/dist/cli/chunk-4TVNJWMA.js.map +0 -1
  62. package/dist/cli/chunk-IAUOP25G.js.map +0 -1
  63. package/dist/cli/chunk-J5BYPUB5.js.map +0 -1
  64. package/dist/cli/chunk-QJG7OF27.js.map +0 -1
  65. package/dist/cli/chunk-TH756VLN.js.map +0 -1
  66. package/dist/cli/code-4TUTAGO5.js.map +0 -1
  67. /package/dist/cli/{chat-A6UJDPGV.js.map → chat-6MBLB7MU.js.map} +0 -0
  68. /package/dist/cli/{chunk-UI66BH6D.js.map → chunk-2UXHZAXP.js.map} +0 -0
  69. /package/dist/cli/{chunk-IX6XI2RG.js.map → chunk-3PNIUDTA.js.map} +0 -0
  70. /package/dist/cli/{chunk-JKGYMRX5.js.map → chunk-676EW5HH.js.map} +0 -0
  71. /package/dist/cli/{chunk-BSINVTTL.js.map → chunk-7R3PKZXB.js.map} +0 -0
  72. /package/dist/cli/{chunk-3T6VBZCL.js.map → chunk-B2RA3NMS.js.map} +0 -0
  73. /package/dist/cli/{chunk-CPKCNHRR.js.map → chunk-BYBOY5UY.js.map} +0 -0
  74. /package/dist/cli/{chunk-3OAR6NVL.js.map → chunk-GG2V37EH.js.map} +0 -0
  75. /package/dist/cli/{chunk-4IBIPQVB.js.map → chunk-J6UWUIT2.js.map} +0 -0
  76. /package/dist/cli/{chunk-T5TQ4NDT.js.map → chunk-MDQWQYBS.js.map} +0 -0
  77. /package/dist/cli/{chunk-ILJOIQ5W.js.map → chunk-NTF4IE7G.js.map} +0 -0
  78. /package/dist/cli/{chunk-XJ5SRLKK.js.map → chunk-OYAIE6C7.js.map} +0 -0
  79. /package/dist/cli/{chunk-BSGCXZQN.js.map → chunk-PT4UDK7Z.js.map} +0 -0
  80. /package/dist/cli/{chunk-WRN65TRD.js.map → chunk-RLQQOIBS.js.map} +0 -0
  81. /package/dist/cli/{chunk-QVC75MR3.js.map → chunk-U7RHC7D6.js.map} +0 -0
  82. /package/dist/cli/{chunk-D5NFKRGO.js.map → chunk-VVQTSZWX.js.map} +0 -0
  83. /package/dist/cli/{chunk-7L2WTRNU.js.map → chunk-X3IHKOYW.js.map} +0 -0
  84. /package/dist/cli/{commands-KMOZEYCF.js.map → commands-PCHFC3CL.js.map} +0 -0
  85. /package/dist/cli/{commit-DTFA56VQ.js.map → commit-HDN6VJBA.js.map} +0 -0
  86. /package/dist/cli/{desktop-7N3MHNBD.js.map → desktop-RHWSCBHO.js.map} +0 -0
  87. /package/dist/cli/{diff-E5OWTF4C.js.map → diff-MV5JNUH4.js.map} +0 -0
  88. /package/dist/cli/{doctor-IEJQRJMN.js.map → doctor-QLO4V4DD.js.map} +0 -0
  89. /package/dist/cli/{mcp-PDI2PDLG.js.map → mcp-JSHFAINM.js.map} +0 -0
  90. /package/dist/cli/{mcp-browse-OSPXOFPZ.js.map → mcp-browse-ESMKKKYH.js.map} +0 -0
  91. /package/dist/cli/{mcp-inspect-QRFVTHMF.js.map → mcp-inspect-WSUN36FM.js.map} +0 -0
  92. /package/dist/cli/{prompt-3CDII3UO.js.map → prompt-5LMDCF4M.js.map} +0 -0
  93. /package/dist/cli/{replay-HYOSRQIV.js.map → replay-D3ILR2YO.js.map} +0 -0
  94. /package/dist/cli/{run-2ZHADOUP.js.map → run-6NN3P5JM.js.map} +0 -0
  95. /package/dist/cli/{server-X75PAZG5.js.map → server-HE7LFAHH.js.map} +0 -0
  96. /package/dist/cli/{sessions-POOZA5CQ.js.map → sessions-HXDBQM3V.js.map} +0 -0
  97. /package/dist/cli/{setup-YLPFI3OH.js.map → setup-EP3UPG3F.js.map} +0 -0
  98. /package/dist/cli/{stats-NXJ3TO2D.js.map → stats-5RC6P5TN.js.map} +0 -0
  99. /package/dist/cli/{version-NXXWE3WN.js.map → version-AIR25TRN.js.map} +0 -0
package/dist/index.js CHANGED
@@ -33,35 +33,35 @@ function defineTheme(base) {
33
33
  }
34
34
  var githubDark = defineTheme({
35
35
  fg: {
36
- strong: "#e6edf3",
37
- body: "#c9d1d9",
38
- sub: "#8b949e",
39
- meta: "#6e7681",
40
- faint: "#484f58"
36
+ strong: "#f8fafc",
37
+ body: "#e5e7eb",
38
+ sub: "#a1a1aa",
39
+ meta: "#71717a",
40
+ faint: "#52525b"
41
41
  },
42
42
  tone: {
43
- brand: "#79c0ff",
44
- accent: "#d2a8ff",
45
- violet: "#b395f5",
46
- ok: "#7ee787",
47
- warn: "#f0b07d",
48
- err: "#ff8b81",
49
- info: "#79c0ff"
43
+ brand: "#f8fafc",
44
+ accent: "#a78bfa",
45
+ violet: "#c084fc",
46
+ ok: "#22c55e",
47
+ warn: "#eab308",
48
+ err: "#ef4444",
49
+ info: "#60a5fa"
50
50
  },
51
51
  toneActive: {
52
- brand: "#a5d6ff",
53
- accent: "#e2c5ff",
54
- violet: "#c8aaff",
55
- ok: "#a8f5ad",
56
- warn: "#ffc99e",
57
- err: "#ffaba3",
58
- info: "#a5d6ff"
52
+ brand: "#ffffff",
53
+ accent: "#c4b5fd",
54
+ violet: "#d8b4fe",
55
+ ok: "#4ade80",
56
+ warn: "#facc15",
57
+ err: "#f87171",
58
+ info: "#93c5fd"
59
59
  },
60
60
  surface: {
61
- bg: "#0a0c10",
62
- bgInput: "#0d1015",
63
- bgCode: "#06080c",
64
- bgElev: "#11141a"
61
+ bg: "#09090b",
62
+ bgInput: "#111113",
63
+ bgCode: "#050507",
64
+ bgElev: "#18181b"
65
65
  }
66
66
  });
67
67
  var dark = defineTheme({
@@ -1139,13 +1139,16 @@ var EN = {
1139
1139
  },
1140
1140
  ui: {
1141
1141
  welcome: "Run `carboncode` any time to start chatting \u2014 your settings are remembered.",
1142
- taglineChat: "DeepSeek-native agent",
1143
- taglineCode: "DeepSeek-native coding agent",
1144
- taglineSub: "cache-first \xB7 flash-first",
1145
- startSessionHint: "type a message to start your session",
1142
+ taglineChat: "terminal coding agent",
1143
+ taglineCode: "terminal coding agent",
1144
+ taglineSub: "read the repo, edit files, run validation",
1145
+ startSessionHint: "type a task to start",
1146
1146
  inputPlaceholder: "Ask anything... (type / for commands, @ for files)",
1147
1147
  busy: "Thinking...",
1148
1148
  thinking: "\u25B8 thinking...",
1149
+ activityWaitingForModel: "waiting for model\u2026",
1150
+ activityThinking: "thinking\u2026",
1151
+ activityProcessing: "processing\u2026",
1149
1152
  undo: "Undo",
1150
1153
  undoHint: "press u within 5s to undo",
1151
1154
  applied: "applied",
@@ -1623,6 +1626,7 @@ var EN = {
1623
1626
  app: {
1624
1627
  walkCancelledRemaining: "\u25B8 walk cancelled \u2014 {count} block(s) still pending.",
1625
1628
  walkCancelled: "\u25B8 walk cancelled.",
1629
+ turnInterrupted: "\u25B8 turn interrupted \u2014 type a follow-up to continue.",
1626
1630
  editModeYolo: "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run. /undo still rolls back edits. Use carefully.",
1627
1631
  editModeAuto: "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo (space pauses the timer). Shell commands still ask.",
1628
1632
  editModeReview: "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)",
@@ -2103,10 +2107,13 @@ var EN = {
2103
2107
  queuedApplyDiscard: "{count} queued \xB7 y apply \xB7 n discard",
2104
2108
  editsQueued: "edits queued \xB7 y apply \xB7 n discard",
2105
2109
  shiftTabFlip: " {mid} \xB7 Shift+Tab to flip",
2106
- queuedDots: "queued\u2026"
2110
+ queuedDots: "queued\u2026",
2111
+ undoApplied: "applied {ok}/{total}",
2112
+ undoHint: "u undo \xB7 Space pause",
2113
+ undoPausedHint: "u undo \xB7 Space resume"
2107
2114
  },
2108
2115
  composer: {
2109
- placeholder: "ask anything \xB7 slash for commands \xB7 at-sign for files",
2116
+ placeholder: "Ask for a code change",
2110
2117
  waitingForResponse: "\u2026waiting for response\u2026",
2111
2118
  hintSend: "send",
2112
2119
  hintNewline: "newline",
@@ -2130,7 +2137,7 @@ var EN = {
2130
2137
  denyTitle: "Deny \u2014 provide context",
2131
2138
  optional: "optional",
2132
2139
  denyFooter: "type context \xB7 \u23CE submit with reason \xB7 esc skip (deny without reason)",
2133
- pickFooter: "\u2191\u2193 pick \xB7 \u23CE confirm \xB7 Tab add context \xB7 esc cancel",
2140
+ pickFooter: "\u2191\u2193 pick \xB7 \u23CE confirm \xB7 esc cancel",
2134
2141
  allowOnce: "allow once",
2135
2142
  allowOnceDesc: "permit this access; remember the directory for the rest of this session",
2136
2143
  allowAlways: "allow always",
@@ -2150,7 +2157,7 @@ var EN = {
2150
2157
  optional: "optional",
2151
2158
  denyFooter: "type context \xB7 \u23CE submit with reason \xB7 esc skip (deny without reason)",
2152
2159
  awaiting: "awaiting",
2153
- pickFooter: "\u2191\u2193 pick \xB7 \u23CE confirm \xB7 Tab add context \xB7 esc cancel",
2160
+ pickFooter: "\u2191\u2193 pick \xB7 \u23CE confirm \xB7 esc cancel",
2154
2161
  allowOnce: "allow once",
2155
2162
  allowOnceDesc: "run this command, ask again next time",
2156
2163
  allowAlways: "allow always",
@@ -2164,7 +2171,7 @@ var EN = {
2164
2171
  previewMorePlural: "\u2026 {n} more lines hidden \u2014 press esc, ask the model to split it"
2165
2172
  },
2166
2173
  editConfirm: {
2167
- footer: "[y/Enter] apply \xB7 [n] reject with reason \xB7 [a] apply rest \xB7 [A] flip AUTO \xB7 [\u2191\u2193/Space] scroll \xB7 [Esc] abort",
2174
+ footer: "Enter apply \xB7 n reject \xB7 \u2191\u2193 scroll \xB7 Esc cancel",
2168
2175
  newTag: "NEW",
2169
2176
  editTag: "EDIT",
2170
2177
  linesCount: "-{removed} +{added} lines",
@@ -2583,13 +2590,16 @@ var zhCN = {
2583
2590
  },
2584
2591
  ui: {
2585
2592
  welcome: "\u968F\u65F6\u8FD0\u884C `carboncode` \u5F00\u59CB\u804A\u5929 \u2014 \u60A8\u7684\u8BBE\u7F6E\u5C06\u88AB\u8BB0\u4F4F\u3002",
2586
- taglineChat: "DeepSeek \u539F\u751F\u667A\u80FD\u4F53",
2587
- taglineCode: "DeepSeek \u539F\u751F\u4EE3\u7801\u667A\u80FD\u4F53",
2588
- taglineSub: "\u7F13\u5B58\u4F18\u5148 \xB7 Flash \u4F18\u5148",
2589
- startSessionHint: "\u8F93\u5165\u6D88\u606F\u4EE5\u5F00\u59CB\u60A8\u7684\u4F1A\u8BDD",
2593
+ taglineChat: "\u7EC8\u7AEF\u4EE3\u7801\u667A\u80FD\u4F53",
2594
+ taglineCode: "\u7EC8\u7AEF\u4EE3\u7801\u667A\u80FD\u4F53",
2595
+ taglineSub: "\u8BFB\u53D6\u4ED3\u5E93\u3001\u7F16\u8F91\u6587\u4EF6\u3001\u8FD0\u884C\u9A8C\u8BC1",
2596
+ startSessionHint: "\u8F93\u5165\u4EFB\u52A1\u5F00\u59CB",
2590
2597
  inputPlaceholder: "\u8F93\u5165\u4EFB\u4F55\u5185\u5BB9... (\u8F93\u5165 / \u4F7F\u7528\u547D\u4EE4, @ \u5F15\u7528\u6587\u4EF6)",
2591
2598
  busy: "\u601D\u8003\u4E2D...",
2592
2599
  thinking: "\u25B8 \u601D\u8003\u4E2D...",
2600
+ activityWaitingForModel: "\u7B49\u5F85\u6A21\u578B\u2026",
2601
+ activityThinking: "\u601D\u8003\u4E2D\u2026",
2602
+ activityProcessing: "\u5904\u7406\u4E2D\u2026",
2593
2603
  undo: "\u64A4\u6D88",
2594
2604
  undoHint: "\u5728 5 \u79D2\u5185\u6309 u \u64A4\u6D88",
2595
2605
  applied: "\u5DF2\u5E94\u7528",
@@ -3068,6 +3078,7 @@ var zhCN = {
3068
3078
  app: {
3069
3079
  walkCancelledRemaining: "\u25B8 \u6D4F\u89C8\u5DF2\u53D6\u6D88 \u2014 \u8FD8\u6709 {count} \u4E2A\u5F85\u5904\u7406\u7F16\u8F91\u5757\u3002",
3070
3080
  walkCancelled: "\u25B8 \u6D4F\u89C8\u5DF2\u53D6\u6D88\u3002",
3081
+ turnInterrupted: "\u25B8 \u672C\u8F6E\u5DF2\u4E2D\u65AD \u2014 \u8F93\u5165\u540E\u7EED\u5185\u5BB9\u53EF\u7EE7\u7EED\u3002",
3071
3082
  editModeYolo: "\u25B8 \u7F16\u8F91\u6A21\u5F0F\uFF1AYOLO \u2014 \u7F16\u8F91\u548C shell \u547D\u4EE4\u90FD\u81EA\u52A8\u6267\u884C\u3002/undo \u4ECD\u53EF\u64A4\u9500\u7F16\u8F91\u3002\u8BF7\u8C28\u614E\u4F7F\u7528\u3002",
3072
3083
  editModeAuto: "\u25B8 \u7F16\u8F91\u6A21\u5F0F\uFF1AAUTO \u2014 \u7F16\u8F91\u7ACB\u5373\u5E94\u7528\uFF1B5 \u79D2\u5185\u6309 u \u64A4\u9500\uFF08\u7A7A\u683C\u6682\u505C\u8BA1\u65F6\uFF09\u3002shell \u547D\u4EE4\u4ECD\u4F1A\u8BE2\u95EE\u3002",
3073
3084
  editModeReview: "\u25B8 \u7F16\u8F91\u6A21\u5F0F\uFF1Areview \u2014 \u7F16\u8F91\u5165\u961F\u5F85 /apply\uFF08\u6216 y\uFF09/ /discard\uFF08\u6216 n\uFF09",
@@ -3548,10 +3559,13 @@ var zhCN = {
3548
3559
  queuedApplyDiscard: "{count} \u4E2A\u5F85\u5904\u7406 \xB7 y \u5E94\u7528 \xB7 n \u4E22\u5F03",
3549
3560
  editsQueued: "\u7F16\u8F91\u5DF2\u6392\u961F \xB7 y \u5E94\u7528 \xB7 n \u4E22\u5F03",
3550
3561
  shiftTabFlip: " {mid} \xB7 Shift+Tab \u5207\u6362",
3551
- queuedDots: "\u6392\u961F\u4E2D\u2026"
3562
+ queuedDots: "\u6392\u961F\u4E2D\u2026",
3563
+ undoApplied: "\u5DF2\u5E94\u7528 {ok}/{total}",
3564
+ undoHint: "u \u64A4\u6D88 \xB7 Space \u6682\u505C",
3565
+ undoPausedHint: "u \u64A4\u6D88 \xB7 Space \u7EE7\u7EED"
3552
3566
  },
3553
3567
  composer: {
3554
- placeholder: "\u8F93\u5165\u4EFB\u4F55\u5185\u5BB9 \xB7 / \u4F7F\u7528\u547D\u4EE4 \xB7 @ \u5F15\u7528\u6587\u4EF6",
3568
+ placeholder: "\u8F93\u5165\u4EFB\u52A1",
3555
3569
  waitingForResponse: "\u2026\u7B49\u5F85\u54CD\u5E94\u2026",
3556
3570
  hintSend: "\u53D1\u9001",
3557
3571
  hintNewline: "\u6362\u884C",
@@ -3575,7 +3589,7 @@ var zhCN = {
3575
3589
  denyTitle: "\u62D2\u7EDD \u2014 \u63D0\u4F9B\u539F\u56E0",
3576
3590
  optional: "\u53EF\u9009",
3577
3591
  denyFooter: "\u8F93\u5165\u539F\u56E0 \xB7 \u23CE \u63D0\u4EA4 \xB7 Esc \u8DF3\u8FC7\uFF08\u76F4\u63A5\u62D2\u7EDD\uFF09",
3578
- pickFooter: "\u2191\u2193 \u9009\u62E9 \xB7 \u23CE \u786E\u8BA4 \xB7 Tab \u6DFB\u52A0\u8BF4\u660E \xB7 Esc \u53D6\u6D88",
3592
+ pickFooter: "\u2191\u2193 \u9009\u62E9 \xB7 \u23CE \u786E\u8BA4 \xB7 Esc \u53D6\u6D88",
3579
3593
  allowOnce: "\u5141\u8BB8\u4E00\u6B21",
3580
3594
  allowOnceDesc: "\u672C\u6B21\u5141\u8BB8\uFF0C\u672C\u4F1A\u8BDD\u5185\u6B64\u76EE\u5F55\u4E0D\u518D\u8BE2\u95EE",
3581
3595
  allowAlways: "\u59CB\u7EC8\u5141\u8BB8",
@@ -3595,7 +3609,7 @@ var zhCN = {
3595
3609
  optional: "\u53EF\u9009",
3596
3610
  denyFooter: "\u8F93\u5165\u539F\u56E0 \xB7 \u23CE \u63D0\u4EA4 \xB7 Esc \u8DF3\u8FC7\uFF08\u76F4\u63A5\u62D2\u7EDD\uFF09",
3597
3611
  awaiting: "\u7B49\u5F85\u4E2D",
3598
- pickFooter: "\u2191\u2193 \u9009\u62E9 \xB7 \u23CE \u786E\u8BA4 \xB7 Tab \u6DFB\u52A0\u8BF4\u660E \xB7 Esc \u53D6\u6D88",
3612
+ pickFooter: "\u2191\u2193 \u9009\u62E9 \xB7 \u23CE \u786E\u8BA4 \xB7 Esc \u53D6\u6D88",
3599
3613
  allowOnce: "\u5141\u8BB8\u4E00\u6B21",
3600
3614
  allowOnceDesc: "\u6267\u884C\u6B64\u547D\u4EE4\uFF0C\u4E0B\u6B21\u518D\u95EE",
3601
3615
  allowAlways: "\u59CB\u7EC8\u5141\u8BB8",
@@ -3609,7 +3623,7 @@ var zhCN = {
3609
3623
  previewMorePlural: "\u2026 \u8FD8\u6709 {n} \u884C\u672A\u663E\u793A \u2014 \u6309 esc \u53D6\u6D88\uFF0C\u8BA9\u6A21\u578B\u62C6\u5206\u540E\u518D\u8BD5"
3610
3624
  },
3611
3625
  editConfirm: {
3612
- footer: "[y/Enter] \u5E94\u7528 \xB7 [n] \u62D2\u7EDD\u5E76\u8BF4\u660E \xB7 [a] \u5E94\u7528\u5269\u4F59 \xB7 [A] \u5207\u6362 AUTO \xB7 [\u2191\u2193/Space] \u6EDA\u52A8 \xB7 [Esc] \u4E2D\u6B62",
3626
+ footer: "Enter \u5E94\u7528 \xB7 n \u62D2\u7EDD \xB7 \u2191\u2193 \u6EDA\u52A8 \xB7 Esc \u53D6\u6D88",
3613
3627
  newTag: "\u65B0\u589E",
3614
3628
  editTag: "\u7F16\u8F91",
3615
3629
  linesCount: "-{removed} +{added} \u884C",
@@ -4785,9 +4799,14 @@ var ToolRegistry = class {
4785
4799
  rejectedReason: "plan-mode"
4786
4800
  });
4787
4801
  }
4802
+ const dispatchCtx = {
4803
+ signal: opts.signal,
4804
+ confirmationGate: opts.confirmationGate,
4805
+ onInteractiveWait: opts.onInteractiveWait
4806
+ };
4788
4807
  if (this._interceptor) {
4789
4808
  try {
4790
- const short = await this._interceptor(name, args);
4809
+ const short = await this._interceptor(name, args, dispatchCtx);
4791
4810
  if (typeof short === "string") return short;
4792
4811
  } catch (err) {
4793
4812
  return JSON.stringify({
@@ -4801,10 +4820,7 @@ var ToolRegistry = class {
4801
4820
  this._auditListener?.({ name, args });
4802
4821
  } catch {
4803
4822
  }
4804
- const result = await tool.fn(args, {
4805
- signal: opts.signal,
4806
- confirmationGate: opts.confirmationGate
4807
- });
4823
+ const result = await tool.fn(args, dispatchCtx);
4808
4824
  const str = typeof result === "string" ? result : JSON.stringify(result);
4809
4825
  let clipped = str;
4810
4826
  if (opts.maxResultTokens !== void 0) {
@@ -6834,6 +6850,8 @@ var CacheFirstLoop = class {
6834
6850
  const name = call.function?.name ?? "";
6835
6851
  const args = call.function?.arguments ?? "{}";
6836
6852
  const parsedArgs = safeParseToolArgs(args);
6853
+ const startedAt = Date.now();
6854
+ let interactiveWaitMs = 0;
6837
6855
  this._inflight.add(this.inflightIdFor(call));
6838
6856
  try {
6839
6857
  const preReport = await runHooks({
@@ -6853,13 +6871,17 @@ var CacheFirstLoop = class {
6853
6871
  preWarnings,
6854
6872
  postWarnings: [],
6855
6873
  result: `[hook block] ${blocking?.hook.command ?? "<unknown>"}
6856
- ${reason}`
6874
+ ${reason}`,
6875
+ elapsedMs: Date.now() - startedAt
6857
6876
  };
6858
6877
  }
6859
6878
  const result = await this.tools.dispatch(name, args, {
6860
6879
  signal,
6861
6880
  maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
6862
- confirmationGate: this.confirmationGate
6881
+ confirmationGate: this.confirmationGate,
6882
+ onInteractiveWait: (elapsedMs) => {
6883
+ interactiveWaitMs += Math.max(0, elapsedMs);
6884
+ }
6863
6885
  });
6864
6886
  const postReport = await runHooks({
6865
6887
  hooks: this.hooks,
@@ -6872,7 +6894,12 @@ ${reason}`
6872
6894
  }
6873
6895
  });
6874
6896
  const postWarnings = [...hookWarnings(postReport.outcomes, this._turn)];
6875
- return { preWarnings, postWarnings, result };
6897
+ return {
6898
+ preWarnings,
6899
+ postWarnings,
6900
+ result,
6901
+ elapsedMs: Math.max(0, Date.now() - startedAt - interactiveWaitMs)
6902
+ };
6876
6903
  } finally {
6877
6904
  this._inflight.delete(this.inflightIdFor(call));
6878
6905
  }
@@ -7357,12 +7384,14 @@ ${reason}`
7357
7384
  const args = call.function?.arguments ?? "{}";
7358
7385
  const s = settled[k];
7359
7386
  let result;
7387
+ let elapsedMs = 0;
7360
7388
  let preWarnings = [];
7361
7389
  let postWarnings = [];
7362
7390
  if (s.status === "fulfilled") {
7363
7391
  preWarnings = s.value.preWarnings;
7364
7392
  postWarnings = s.value.postWarnings;
7365
7393
  result = s.value.result;
7394
+ elapsedMs = s.value.elapsedMs;
7366
7395
  } else {
7367
7396
  const err = s.reason instanceof Error ? s.reason : new Error(String(s.reason));
7368
7397
  result = JSON.stringify({ error: `${err.name}: ${err.message}` });
@@ -7381,7 +7410,8 @@ ${reason}`
7381
7410
  content: result,
7382
7411
  toolName: name,
7383
7412
  toolArgs: args,
7384
- callId: this.inflightIdFor(call)
7413
+ callId: this.inflightIdFor(call),
7414
+ elapsedMs
7385
7415
  };
7386
7416
  }
7387
7417
  }
@@ -8931,27 +8961,51 @@ async function applyMultiEdit(rootDir, edits) {
8931
8961
  );
8932
8962
  }
8933
8963
  const rel = displayRel(rootDir, e.abs);
8934
- if (e.search.length === 0) {
8935
- throw new Error(
8936
- `multi_edit: edit #${i + 1} (${rel}) search cannot be empty (no edits applied)`
8937
- );
8938
- }
8939
8964
  let state = filesByPath.get(e.abs);
8940
8965
  if (!state) {
8941
8966
  let before;
8942
- try {
8943
- before = await fs.readFile(e.abs, "utf8");
8944
- } catch (err) {
8945
- throw new Error(
8946
- `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
8947
- );
8967
+ if (e.search.length === 0) {
8968
+ try {
8969
+ await fs.readFile(e.abs, "utf8");
8970
+ throw new Error("empty SEARCH only creates new files \u2014 this file already exists");
8971
+ } catch (err) {
8972
+ const code = err.code;
8973
+ if (code !== "ENOENT") {
8974
+ throw new Error(
8975
+ `multi_edit: edit #${i + 1} cannot create ${rel}: ${err.message} (no edits applied)`
8976
+ );
8977
+ }
8978
+ }
8979
+ state = { buf: "", le: "\n", hunks: [], deltaChars: 0, touched: 0, created: true };
8980
+ filesByPath.set(e.abs, state);
8981
+ } else {
8982
+ try {
8983
+ before = await fs.readFile(e.abs, "utf8");
8984
+ } catch (err) {
8985
+ throw new Error(
8986
+ `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
8987
+ );
8988
+ }
8989
+ const le = before.includes("\r\n") ? "\r\n" : "\n";
8990
+ state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0, created: false };
8991
+ filesByPath.set(e.abs, state);
8948
8992
  }
8949
- const le = before.includes("\r\n") ? "\r\n" : "\n";
8950
- state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
8951
- filesByPath.set(e.abs, state);
8993
+ }
8994
+ if (e.search.length === 0 && (!state.created || state.touched > 0)) {
8995
+ throw new Error(
8996
+ `multi_edit: edit #${i + 1} (${rel}) empty search only creates new files (no edits applied)`
8997
+ );
8952
8998
  }
8953
8999
  const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
8954
9000
  const adaptedReplace = e.replace.replace(/\r?\n/g, state.le);
9001
+ if (adaptedSearch.length === 0) {
9002
+ state.buf = adaptedReplace;
9003
+ state.hunks.push(`# ${rel}
9004
+ ${renderCreateDiff(adaptedReplace)}`);
9005
+ state.deltaChars += adaptedReplace.length;
9006
+ state.touched++;
9007
+ continue;
9008
+ }
8955
9009
  const firstIdx = state.buf.indexOf(adaptedSearch);
8956
9010
  if (firstIdx < 0) {
8957
9011
  throw new Error(
@@ -8972,6 +9026,7 @@ ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
8972
9026
  state.touched++;
8973
9027
  }
8974
9028
  for (const [abs, state] of filesByPath) {
9029
+ if (state.created) await fs.mkdir(pathMod.dirname(abs), { recursive: true });
8975
9030
  await fs.writeFile(abs, state.buf, "utf8");
8976
9031
  }
8977
9032
  const fileCount = filesByPath.size;
@@ -8998,6 +9053,13 @@ function renderEditDiff(search, replace, startLine) {
8998
9053
  return `${hunk}
8999
9054
  ${body}`;
9000
9055
  }
9056
+ function renderCreateDiff(replace) {
9057
+ const lines = replace.length === 0 ? [] : replace.split(/\r?\n/);
9058
+ const hunk = `@@ -1,0 +1,${lines.length} @@`;
9059
+ const body = lines.map((line) => `+ ${line}`).join("\n");
9060
+ return body ? `${hunk}
9061
+ ${body}` : hunk;
9062
+ }
9001
9063
  function lineDiff(a, b) {
9002
9064
  const n = a.length;
9003
9065
  const m = b.length;
@@ -9281,6 +9343,110 @@ function formatOutline(entries) {
9281
9343
  ].join("\n");
9282
9344
  }
9283
9345
 
9346
+ // src/tools/fs/patch.ts
9347
+ function parseUnifiedPatch(patch) {
9348
+ if (typeof patch !== "string" || patch.trim().length === 0) {
9349
+ throw new Error("apply_patch: patch must be a non-empty string");
9350
+ }
9351
+ const lines = patch.split(/\n/);
9352
+ const files = [];
9353
+ let current = null;
9354
+ const ensureCurrent = () => {
9355
+ if (!current) {
9356
+ current = { oldPath: null, newPath: null, blocks: [] };
9357
+ files.push(current);
9358
+ }
9359
+ return current;
9360
+ };
9361
+ let i = 0;
9362
+ while (i < lines.length) {
9363
+ const line = lines[i];
9364
+ if (line.startsWith("diff --git ")) {
9365
+ current = { oldPath: null, newPath: null, blocks: [] };
9366
+ files.push(current);
9367
+ i++;
9368
+ continue;
9369
+ }
9370
+ if (line.startsWith("--- ")) {
9371
+ ensureCurrent().oldPath = parsePatchPath(line.slice(4));
9372
+ i++;
9373
+ continue;
9374
+ }
9375
+ if (line.startsWith("+++ ")) {
9376
+ ensureCurrent().newPath = parsePatchPath(line.slice(4));
9377
+ i++;
9378
+ continue;
9379
+ }
9380
+ if (line.startsWith("@@")) {
9381
+ const file = ensureCurrent();
9382
+ const path2 = file.newPath ?? file.oldPath;
9383
+ if (!path2) throw new Error("apply_patch: hunk is missing a file path");
9384
+ const searchLines = [];
9385
+ const replaceLines = [];
9386
+ i++;
9387
+ while (i < lines.length) {
9388
+ const hunkLine = lines[i];
9389
+ if (hunkLine.startsWith("diff --git ") || hunkLine.startsWith("@@") || hunkLine.startsWith("--- ")) {
9390
+ break;
9391
+ }
9392
+ if (hunkLine === "" && i === lines.length - 1) {
9393
+ break;
9394
+ }
9395
+ if (hunkLine.startsWith("\")) {
9396
+ i++;
9397
+ continue;
9398
+ }
9399
+ const marker = hunkLine[0];
9400
+ const body = hunkLine.slice(1);
9401
+ if (marker === " ") {
9402
+ searchLines.push(body);
9403
+ replaceLines.push(body);
9404
+ } else if (marker === "-") {
9405
+ searchLines.push(body);
9406
+ } else if (marker === "+") {
9407
+ replaceLines.push(body);
9408
+ } else if (hunkLine.trim().length === 0) {
9409
+ } else {
9410
+ throw new Error(`apply_patch: invalid hunk line: ${hunkLine}`);
9411
+ }
9412
+ i++;
9413
+ }
9414
+ file.blocks.push({
9415
+ path: path2,
9416
+ search: file.oldPath === null ? "" : linesToText(searchLines),
9417
+ replace: linesToText(replaceLines),
9418
+ offset: 0
9419
+ });
9420
+ continue;
9421
+ }
9422
+ i++;
9423
+ }
9424
+ const blocks = files.flatMap((file) => file.blocks);
9425
+ if (blocks.length === 0) throw new Error("apply_patch: patch did not contain any hunks");
9426
+ return blocks;
9427
+ }
9428
+ function linesToText(lines) {
9429
+ if (lines.length === 0) return "";
9430
+ return `${lines.join("\n")}
9431
+ `;
9432
+ }
9433
+ function parsePatchPath(raw) {
9434
+ const cleaned = unquotePath(raw.trim().split(/\t/, 1)[0] ?? "");
9435
+ if (cleaned === "/dev/null") return null;
9436
+ const withoutPrefix = cleaned.replace(/^[ab]\//, "");
9437
+ const normalized = withoutPrefix.replace(/^[/\\]+/, "");
9438
+ if (!normalized) throw new Error(`apply_patch: invalid patch path: ${raw}`);
9439
+ return normalized.replaceAll("\\", "/");
9440
+ }
9441
+ function unquotePath(path2) {
9442
+ if (path2.length < 2 || !path2.startsWith('"') || !path2.endsWith('"')) return path2;
9443
+ try {
9444
+ return JSON.parse(path2);
9445
+ } catch {
9446
+ return path2.slice(1, -1);
9447
+ }
9448
+ }
9449
+
9284
9450
  // src/tools/fs/search.ts
9285
9451
  import { promises as fs3 } from "fs";
9286
9452
  import * as pathMod4 from "path";
@@ -9578,10 +9744,14 @@ ${body}`;
9578
9744
  let pending = inflightGate.get(allowPrefix);
9579
9745
  if (!pending) {
9580
9746
  const gate = ctx?.confirmationGate ?? pauseGate;
9747
+ const waitStartedAt = Date.now();
9581
9748
  pending = gate.ask({
9582
9749
  kind: "path_access",
9583
9750
  payload: { path: abs, intent, toolName, sandboxRoot: normRoot, allowPrefix }
9584
9751
  });
9752
+ pending = pending.finally(() => {
9753
+ ctx?.onInteractiveWait?.(Date.now() - waitStartedAt);
9754
+ });
9585
9755
  inflightGate.set(allowPrefix, pending);
9586
9756
  void pending.finally(() => inflightGate.delete(allowPrefix));
9587
9757
  }
@@ -10001,7 +10171,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10001
10171
  });
10002
10172
  registry.register({
10003
10173
  name: "multi_edit",
10004
- description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in a single atomic call. Edits run sequentially in array order; for edits that touch the same file, a later edit can match text inserted by an earlier one. If ANY edit fails (search not found, ambiguous match, empty search, file unreadable), NO files are written \u2014 atomic at the validation layer. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
10174
+ description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in a single atomic call. Edits run sequentially in array order; for edits that touch the same file, a later edit can match text inserted by an earlier one. If ANY edit fails (search not found, ambiguous match, file unreadable, invalid create), NO files are written \u2014 atomic at the validation layer. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. To create a new file, use an empty `search`; empty `search` is refused for existing files. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
10005
10175
  parameters: {
10006
10176
  type: "object",
10007
10177
  properties: {
@@ -10038,6 +10208,41 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
10038
10208
  return applyMultiEdit(rootDir, resolved);
10039
10209
  }
10040
10210
  });
10211
+ registry.register({
10212
+ name: "apply_patch",
10213
+ description: "Apply a unified git-style patch atomically under the sandbox root. Use this for non-trivial code edits because it lets the UI review the whole patch as one batch. If any hunk fails, no files are written.",
10214
+ parameters: {
10215
+ type: "object",
10216
+ properties: {
10217
+ patch: {
10218
+ type: "string",
10219
+ description: "Unified diff text with diff --git / --- / +++ headers and @@ hunks. Paths should be project-relative or a/ b/-prefixed."
10220
+ }
10221
+ },
10222
+ required: ["patch"]
10223
+ },
10224
+ fn: async (args, ctx) => {
10225
+ try {
10226
+ const blocks = parseUnifiedPatch(args.patch);
10227
+ const resolved = await Promise.all(
10228
+ blocks.map(async (block) => ({
10229
+ abs: await safePath(block.path, "apply_patch", ctx, "write"),
10230
+ search: block.search,
10231
+ replace: block.replace,
10232
+ rel: block.path,
10233
+ created: block.search.length === 0
10234
+ }))
10235
+ );
10236
+ const output = await applyMultiEdit(rootDir, resolved);
10237
+ const fileCount = new Set(resolved.map((edit) => edit.rel)).size;
10238
+ const fileNoun = fileCount === 1 ? "file" : "files";
10239
+ const created = resolved.filter((edit) => edit.created).map((edit) => `created ${edit.rel}`);
10240
+ return [`apply_patch: applied ${fileCount} ${fileNoun}`, ...created, output].join("\n");
10241
+ } catch (err) {
10242
+ return `apply_patch: no files written \u2014 ${err.message}`;
10243
+ }
10244
+ }
10245
+ });
10041
10246
  registry.register({
10042
10247
  name: "create_directory",
10043
10248
  description: "Create a directory (and any missing parents) under the sandbox root.",
@@ -12269,7 +12474,7 @@ function hasSensitivePathArgs(argv, projectRoot, extraPrefixes = [], extraPatter
12269
12474
  }
12270
12475
  return false;
12271
12476
  }
12272
- function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
12477
+ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig, opts = {}) {
12273
12478
  let argv;
12274
12479
  try {
12275
12480
  argv = tokenizeCommand(cmd);
@@ -12277,7 +12482,7 @@ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
12277
12482
  return false;
12278
12483
  }
12279
12484
  if (argv.length === 0) return false;
12280
- const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
12485
+ const allowlist = [...opts.includeBuiltin === false ? [] : BUILTIN_ALLOWLIST, ...extra];
12281
12486
  for (const prefix of allowlist) {
12282
12487
  const prefixTokens = prefix.split(" ");
12283
12488
  if (argv.length < prefixTokens.length) continue;
@@ -12302,15 +12507,18 @@ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
12302
12507
  }
12303
12508
  return false;
12304
12509
  }
12305
- function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
12510
+ function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig, opts = {}) {
12306
12511
  let chain;
12307
12512
  try {
12308
12513
  chain = parseCommandChain(cmd);
12309
12514
  } catch {
12310
12515
  return false;
12311
12516
  }
12312
- if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig);
12313
- return chainAllowed(chain, (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig));
12517
+ if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig, opts);
12518
+ return chainAllowed(
12519
+ chain,
12520
+ (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig, opts)
12521
+ );
12314
12522
  }
12315
12523
 
12316
12524
  // src/tools/shell/exec.ts
@@ -12610,9 +12818,19 @@ function registerShellTools(registry, opts) {
12610
12818
  return () => snapshot2;
12611
12819
  })();
12612
12820
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
12821
+ const isAutoAllowed = (cmd) => isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths, {
12822
+ includeBuiltin: opts.requireApprovalForBuiltin !== true
12823
+ });
12824
+ const approvalPolicy = opts.requireApprovalForBuiltin ? "Model-requested shell commands ask the user before they run unless the user has explicitly always-allowed a project prefix or yolo mode is active. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify." : "Allowlisted read-only / test / lint / typecheck commands run immediately; anything that could mutate state, install deps, or touch the network is gated by user confirmation. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.";
12613
12825
  registry.register({
12614
12826
  name: "run_command",
12615
- description: "Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; anything that could mutate state, install deps, or touch the network is gated by user confirmation. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.\n\nConstraints (no real shell \u2014 argv is parsed natively for cross-platform parity):\n\u2022 Supported: chain ops `|` / `||` / `&&` / `;` (each segment allowlist-checked individually), file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` (target paths resolve relative to project root, max one redirect per fd per segment).\n\u2022 NOT supported: background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, process substitution `<(\u2026)`, `$VAR` env expansion, glob expansion. To pass an operator char as literal arg, quote it (`grep \"a|b\" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain like `cd dir && cmd`. Use the binary's own cwd flag: `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>`, `pytest <dir>/tests`.\n\u2022 Filter at source \u2014 unbounded output (`netstat -ano`, `find /`) wastes tokens. Use `grep -c`, `wc -l`, narrower paths, etc.",
12827
+ description: `Run a shell command in the project root; returns combined stdout+stderr. ${approvalPolicy}
12828
+
12829
+ Constraints (no real shell \u2014 argv is parsed natively for cross-platform parity):
12830
+ \u2022 Supported: chain ops \`|\` / \`||\` / \`&&\` / \`;\` (each segment allowlist-checked individually), file redirects \`>\` / \`>>\` / \`<\` / \`2>\` / \`2>>\` / \`2>&1\` / \`&>\` (target paths resolve relative to project root, max one redirect per fd per segment).
12831
+ \u2022 NOT supported: background \`&\`, heredoc \`<<\`, command substitution \`$(\u2026)\`, subshells \`(\u2026)\`, process substitution \`<(\u2026)\`, \`$VAR\` env expansion, glob expansion. To pass an operator char as literal arg, quote it (\`grep "a|b" file\`).
12832
+ \u2022 \`cd\` does NOT persist \u2014 between calls OR within a chain like \`cd dir && cmd\`. Use the binary's own cwd flag: \`npm --prefix <dir>\`, \`git -C <dir>\`, \`cargo -C <dir>\`, \`pytest <dir>/tests\`.
12833
+ \u2022 Filter at source \u2014 unbounded output (\`netstat -ano\`, \`find /\`) wastes tokens. Use \`grep -c\`, \`wc -l\`, narrower paths, etc.`,
12616
12834
  // Plan-mode gate: allow allowlisted commands through (git status,
12617
12835
  // cargo check, ls, grep …) so the model can actually investigate
12618
12836
  // during planning. Anything that would otherwise trigger a
@@ -12641,12 +12859,14 @@ function registerShellTools(registry, opts) {
12641
12859
  const cmd = args.command.trim();
12642
12860
  if (!cmd) throw new Error("run_command: empty command");
12643
12861
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
12644
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
12862
+ if (!isAllowAll() && !isAutoAllowed(cmd)) {
12645
12863
  const gate = ctx?.confirmationGate ?? pauseGate;
12864
+ const waitStartedAt = Date.now();
12646
12865
  const choice = await gate.ask({
12647
12866
  kind: "run_command",
12648
12867
  payload: { command: cmd, cwd: rootDir, timeoutSec: effectiveTimeout }
12649
12868
  });
12869
+ ctx?.onInteractiveWait?.(Date.now() - waitStartedAt);
12650
12870
  if (choice.type === "deny") {
12651
12871
  throw new Error(
12652
12872
  `user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
@@ -12690,12 +12910,14 @@ function registerShellTools(registry, opts) {
12690
12910
  const cmd = args.command.trim();
12691
12911
  if (!cmd) throw new Error("run_background: empty command");
12692
12912
  const cwd = resolveCwdInsideRoot(rootDir, args.cwd);
12693
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
12913
+ if (!isAllowAll() && !isAutoAllowed(cmd)) {
12694
12914
  const gate = ctx?.confirmationGate ?? pauseGate;
12915
+ const waitStartedAt = Date.now();
12695
12916
  const choice = await gate.ask({
12696
12917
  kind: "run_background",
12697
12918
  payload: { command: cmd, cwd, waitSec: args.waitSec }
12698
12919
  });
12920
+ ctx?.onInteractiveWait?.(Date.now() - waitStartedAt);
12699
12921
  if (choice.type === "deny") {
12700
12922
  throw new Error(
12701
12923
  `user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
@@ -13766,14 +13988,14 @@ function truncate(s, n) {
13766
13988
  // src/version.ts
13767
13989
  import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
13768
13990
  import { homedir as homedir7 } from "os";
13769
- import { dirname as dirname7, join as join14 } from "path";
13991
+ import { dirname as dirname8, join as join14 } from "path";
13770
13992
  import { fileURLToPath as fileURLToPath2 } from "url";
13771
13993
  var REGISTRY_URL = "https://registry.npmjs.org/@carboncode/cli/latest";
13772
13994
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
13773
13995
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
13774
13996
  function readPackageVersion() {
13775
13997
  try {
13776
- let dir = dirname7(fileURLToPath2(import.meta.url));
13998
+ let dir = dirname8(fileURLToPath2(import.meta.url));
13777
13999
  for (let i = 0; i < 6; i++) {
13778
14000
  const p = join14(dir, "package.json");
13779
14001
  if (existsSync10(p)) {
@@ -13782,7 +14004,7 @@ function readPackageVersion() {
13782
14004
  return pkg.version;
13783
14005
  }
13784
14006
  }
13785
- const parent = dirname7(dir);
14007
+ const parent = dirname8(dir);
13786
14008
  if (parent === dir) break;
13787
14009
  dir = parent;
13788
14010
  }
@@ -13808,7 +14030,7 @@ function readCache(homeDirOverride) {
13808
14030
  function writeCache(entry, homeDirOverride) {
13809
14031
  try {
13810
14032
  const p = cachePath(homeDirOverride);
13811
- mkdirSync5(dirname7(p), { recursive: true });
14033
+ mkdirSync5(dirname8(p), { recursive: true });
13812
14034
  writeFileSync5(p, JSON.stringify(entry), "utf8");
13813
14035
  } catch {
13814
14036
  }
@@ -14578,7 +14800,7 @@ import {
14578
14800
  writeFileSync as writeFileSync6,
14579
14801
  writeSync
14580
14802
  } from "fs";
14581
- import { dirname as dirname8, resolve as resolve12 } from "path";
14803
+ import { dirname as dirname9, resolve as resolve12 } from "path";
14582
14804
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
14583
14805
  function parseEditBlocks(text) {
14584
14806
  const out = [];
@@ -14608,7 +14830,7 @@ function applyEditBlock(block, rootDir) {
14608
14830
  const searchEmpty = block.search.length === 0;
14609
14831
  if (searchEmpty) {
14610
14832
  try {
14611
- mkdirSync6(dirname8(absTarget), { recursive: true });
14833
+ mkdirSync6(dirname9(absTarget), { recursive: true });
14612
14834
  const fd = openSync2(absTarget, "wx");
14613
14835
  try {
14614
14836
  writeSync(fd, block.replace);
@@ -14748,7 +14970,7 @@ var DEFAULT_CODE_MODEL = "deepseek-v4-flash";
14748
14970
  function codeSystemBase(modelId) {
14749
14971
  return CODE_SYSTEM_TEMPLATE.replace("__ESCALATION_CONTRACT__", escalationContract(modelId));
14750
14972
  }
14751
- var CODE_SYSTEM_TEMPLATE = `You are Carbon Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, multi_edit, list_directory, directory_tree, search_files, search_content, glob, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell, plus \`todo_write\` for in-session multi-step tracking.
14973
+ var CODE_SYSTEM_TEMPLATE = `You are Carbon Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, multi_edit, apply_patch, list_directory, directory_tree, search_files, search_content, glob, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell, plus \`todo_write\` for in-session multi-step tracking.
14752
14974
 
14753
14975
  # Identity is fixed by this prompt \u2014 never inferred from the workspace
14754
14976
 
@@ -14831,8 +15053,8 @@ Call shape: \`{ todos: [{ content, activeForm, status }, ...] }\` \u2014 \`conte
14831
15053
  # Plan mode (/plan)
14832
15054
 
14833
15055
  The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
14834
- - Write tools (edit_file, multi_edit, write_file, create_directory, move_file, copy_file, delete_file, delete_directory) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
14835
- - Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
15056
+ - Write tools (edit_file, multi_edit, apply_patch, write_file, create_directory, move_file, copy_file, delete_file, delete_directory) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
15057
+ - Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work after user approval \u2014 use them to investigate.
14836
15058
  - You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
14837
15059
 
14838
15060
 
@@ -14874,8 +15096,8 @@ When you do propose edits, the user will review them and decide whether to \`/ap
14874
15096
 
14875
15097
  Carbon Code runs an **edit gate**. The user's current mode (\`review\` or \`auto\`) decides what happens to your writes; you DO NOT see which mode is active, and you SHOULD NOT ask. Write the same way in both cases.
14876
15098
 
14877
- - In \`auto\` mode \`edit_file\` / \`write_file\` calls land on disk immediately with an undo window \u2014 you'll get the normal "edit blocks: 1/1 applied" style response.
14878
- - In \`review\` mode EACH \`edit_file\` / \`write_file\` call pauses tool dispatch while the user decides. You'll get one of these responses:
15099
+ - In \`auto\` mode \`edit_file\` / \`write_file\` / \`multi_edit\` / \`apply_patch\` calls land on disk immediately with an undo window \u2014 you'll get the normal applied-edit response.
15100
+ - In \`review\` mode EACH \`edit_file\` / \`write_file\` / \`multi_edit\` / \`apply_patch\` call pauses tool dispatch while the user decides. You'll get one of these responses:
14879
15101
  - \`"edit blocks: 1/1 applied"\` \u2014 user approved it. Continue as normal.
14880
15102
  - \`"User rejected this edit to <path>. Don't retry the same SEARCH/REPLACE\u2026"\` \u2014 user said no to THIS specific edit. Do NOT re-emit the same block, do NOT switch tools to sneak it past the gate (write_file \u2192 edit_file, or text-form SEARCH/REPLACE). Either take a clearly different approach or stop and ask the user what they want instead.
14881
15103
  - Text-form SEARCH/REPLACE blocks in your assistant reply queue for end-of-turn /apply \u2014 same "don't retry on rejection" rule.
@@ -14883,7 +15105,20 @@ Carbon Code runs an **edit gate**. The user's current mode (\`review\` or \`auto
14883
15105
 
14884
15106
  # Editing files
14885
15107
 
14886
- When you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:
15108
+ Default loop: inspect, patch, verify, summarize. After tests pass, summarize briefly and cite the files or commands that matter.
15109
+
15110
+ When you've been asked to change a file, prefer \`apply_patch\` for non-trivial edits. It accepts a unified git-style patch and lets the user review the whole batch at once:
15111
+
15112
+ \`\`\`diff
15113
+ diff --git a/path/to/file.ext b/path/to/file.ext
15114
+ --- a/path/to/file.ext
15115
+ +++ b/path/to/file.ext
15116
+ @@ -1 +1 @@
15117
+ -old line
15118
+ +new line
15119
+ \`\`\`
15120
+
15121
+ For tiny exact replacements, \`edit_file\` is fine. For generated programmatic batches, \`multi_edit\` is also fine. If you are writing text-form edits in your final answer instead of calling tools, output one or more SEARCH/REPLACE blocks in this exact format:
14887
15122
 
14888
15123
  path/to/file.ext
14889
15124
  <<<<<<< SEARCH
@@ -14903,7 +15138,7 @@ Rules:
14903
15138
  >>>>>>> REPLACE
14904
15139
  - Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
14905
15140
  - Paths are relative to the working directory. Don't use absolute paths.
14906
- - For multi-site changes \u2014 same file or across files \u2014 prefer \`multi_edit\` over N \`edit_file\` calls. Shape: \`{ edits: [{ path, search, replace }, ...] }\`. All edits validate before any file is written; any failure \u2192 ALL files untouched. Per-file edits run in array order, so a later edit can match text inserted by an earlier one.
15141
+ - For multi-site changes \u2014 same file or across files \u2014 prefer \`apply_patch\` or \`multi_edit\` over N \`edit_file\` calls. \`multi_edit\` shape: \`{ edits: [{ path, search, replace }, ...] }\`. Both batch tools validate before any file is written; any failure \u2192 ALL files untouched. Per-file edits run in array order, so a later edit can match text inserted by an earlier one. To create a new file inside a \`multi_edit\`, use an empty \`search\` for that file.
14907
15142
 
14908
15143
  # Trust what you already know
14909
15144
 
@@ -14912,7 +15147,7 @@ Before exploring the filesystem to answer a factual question, check whether the
14912
15147
  # Exploration
14913
15148
 
14914
15149
  - Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
14915
- - Prefer \`search_files\` over \`list_directory\` when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees. Note: \`search_files\` matches file NAMES; for searching file CONTENTS use \`search_content\`.
15150
+ - Prefer \`search_files\` over \`list_directory\` when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees. Use \`search_files\` for a filename query. Use \`glob\` for wildcard file patterns. Note: \`search_files\` matches file NAMES; for searching file CONTENTS use \`search_content\`.
14916
15151
  - Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`glob\` (mtime-sorted glob \u2014 use for "what changed lately", "all *.ts under src/"), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"; pass \`context:N\` for grep -C N around hits), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
14917
15152
 
14918
15153
  # Path conventions
@@ -14939,6 +15174,10 @@ You have TWO tools for running shell commands, and picking the right one is non-
14939
15174
 
14940
15175
  **Never use run_command for a dev server or a download likely to exceed a minute.** It will block, time out, and the user will see a frozen tool call while the work was actually running fine. Always \`run_background\` + \`wait_for_job\` / \`job_output\`.
14941
15176
 
15177
+ # Explicit command order
15178
+
15179
+ If the user explicitly asks you to run a command first (for example "\u5148\u8FD0\u884C\u6D4B\u8BD5", "run tests first", or "\u5148\u770B npm test \u5931\u8D25"), do that command before reading files or proposing edits. Do not substitute file reading, test-file inspection, or inference for the requested command execution. If the command is denied, say so and continue only within the user's constraint.
15180
+
14942
15181
  After \`run_background\`, tools available to you:
14943
15182
  - \`job_output(jobId, tailLines?)\` \u2014 read recent logs to verify startup / debug errors.
14944
15183
  - \`wait_for_job(jobId, timeoutMs?, waitFor?)\` \u2014 block server-side until the job finishes (or, with \`waitFor: 'output-or-exit'\`, until it writes a new line). ONE tool call per wait regardless of duration. \`timeoutMs\` clamps at 300_000. For downloads / installs / builds: leave \`waitFor\` at the default \`'exit'\` and set \`timeoutMs\` to the slowest reasonable end-to-end. For tailing a dev server and reacting to a specific log line: pass \`waitFor: 'output-or-exit'\` with a short \`timeoutMs\`.
@@ -15041,7 +15280,7 @@ import {
15041
15280
  writeFileSync as writeFileSync7
15042
15281
  } from "fs";
15043
15282
  import { homedir as homedir8 } from "os";
15044
- import { dirname as dirname9, join as join16 } from "path";
15283
+ import { dirname as dirname10, join as join16 } from "path";
15045
15284
  function defaultUsageLogPath(homeDirOverride) {
15046
15285
  return join16(homeDirOverride ?? homedir8(), ".carboncode", "usage.jsonl");
15047
15286
  }
@@ -15108,7 +15347,7 @@ function appendUsage(input) {
15108
15347
  if (input.subagent) record.subagent = input.subagent;
15109
15348
  const path2 = input.path ?? defaultUsageLogPath();
15110
15349
  try {
15111
- mkdirSync7(dirname9(path2), { recursive: true });
15350
+ mkdirSync7(dirname10(path2), { recursive: true });
15112
15351
  appendFileSync2(path2, `${JSON.stringify(record)}
15113
15352
  `, "utf8");
15114
15353
  compactUsageLogIfLarge(path2, record.ts);