@cuongtran001/kanna 1.63.2 → 1.63.3

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 (199) hide show
  1. package/README.md +5 -9
  2. package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
  3. package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
  4. package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
  5. package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
  6. package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
  7. package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
  8. package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
  9. package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
  10. package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
  11. package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
  12. package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
  13. package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
  14. package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
  15. package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
  16. package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
  17. package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
  18. package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
  19. package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
  20. package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
  21. package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
  22. package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
  23. package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
  24. package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
  25. package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
  26. package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
  27. package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
  28. package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
  29. package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
  30. package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
  31. package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
  32. package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
  33. package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
  34. package/dist/client/assets/index-CxDE9VOy.css +1 -0
  35. package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
  36. package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
  37. package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
  38. package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
  39. package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
  40. package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
  41. package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
  42. package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
  43. package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
  44. package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
  45. package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
  46. package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
  47. package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
  48. package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
  49. package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
  50. package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
  51. package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
  52. package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
  53. package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
  54. package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
  55. package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
  56. package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
  57. package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
  58. package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
  59. package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
  60. package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
  61. package/dist/client/index.html +35 -35
  62. package/package.json +2 -2
  63. package/src/server/agent-coordinator-types.ts +0 -6
  64. package/src/server/agent-coordinator.ts +6 -45
  65. package/src/server/agent.compaction.test.ts +1 -13
  66. package/src/server/agent.oauth-account-info.test.ts +0 -3
  67. package/src/server/agent.oauth-release.test.ts +0 -2
  68. package/src/server/agent.oauth-rotation.test.ts +0 -9
  69. package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
  70. package/src/server/agent.openrouter-model.test.ts +0 -2
  71. package/src/server/agent.openrouter-watchdog.test.ts +0 -3
  72. package/src/server/agent.sdk-workflow-register.test.ts +2 -64
  73. package/src/server/agent.test.ts +1 -319
  74. package/src/server/app-settings.test.ts +14 -17
  75. package/src/server/app-settings.ts +22 -37
  76. package/src/server/auto-continue/e2e.test.ts +0 -1
  77. package/src/server/background-task-output-registry.test.ts +1 -1
  78. package/src/server/background-task-output-registry.ts +1 -1
  79. package/src/server/claude-cancel-handler.test.ts +0 -58
  80. package/src/server/claude-cancel-handler.ts +1 -17
  81. package/src/server/claude-harness-stream.test.ts +134 -0
  82. package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
  83. package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
  84. package/src/server/claude-send-command.test.ts +0 -1
  85. package/src/server/claude-session-config-helpers.test.ts +1 -90
  86. package/src/server/claude-session-config-helpers.ts +1 -38
  87. package/src/server/claude-session-error-handler.test.ts +0 -1
  88. package/src/server/claude-session-lifecycle.test.ts +2 -36
  89. package/src/server/claude-session-lifecycle.ts +2 -8
  90. package/src/server/claude-session-runner.test.ts +8 -36
  91. package/src/server/claude-session-runner.ts +1 -26
  92. package/src/server/claude-session-spawner.test.ts +5 -64
  93. package/src/server/claude-session-spawner.ts +49 -115
  94. package/src/server/claude-session-start.test.ts +1 -14
  95. package/src/server/claude-session-start.ts +0 -8
  96. package/src/server/claude-session-state-queries.test.ts +1 -2
  97. package/src/server/claude-session-state.test.ts +0 -1
  98. package/src/server/claude-session-state.ts +0 -4
  99. package/src/server/claude-subagent-wiring.test.ts +2 -68
  100. package/src/server/claude-subagent-wiring.ts +3 -50
  101. package/src/server/claude-turn-starter-types.ts +0 -3
  102. package/src/server/claude-turn-starter.test.ts +0 -28
  103. package/src/server/claude-turn-starter.ts +0 -5
  104. package/src/server/event-store.test.ts +0 -2
  105. package/src/server/events.ts +1 -2
  106. package/src/server/harness-types.ts +1 -2
  107. package/src/server/import-subagent-drill.e2e.test.ts +1 -1
  108. package/src/server/imported-subagents-dir.test.ts +1 -1
  109. package/src/server/imported-subagents-dir.ts +1 -1
  110. package/src/server/kanna-mcp.test.ts +0 -75
  111. package/src/server/kanna-mcp.ts +1 -47
  112. package/src/server/server.test.ts +5 -7
  113. package/src/server/server.ts +2 -18
  114. package/src/server/subagent-orchestrator.test.ts +0 -1
  115. package/src/server/subagent-provider-run.test.ts +48 -10
  116. package/src/server/subagent-provider-run.ts +12 -3
  117. package/src/server/tool-callback.test.ts +1 -1
  118. package/src/server/ws-router-agent-ctrl.test.ts +0 -45
  119. package/src/server/ws-router-agent-ctrl.ts +1 -31
  120. package/src/server/ws-router-broadcast.ts +0 -27
  121. package/src/server/ws-router-defaults.test.ts +2 -3
  122. package/src/server/ws-router-defaults.ts +2 -3
  123. package/src/server/ws-router-envelope.ts +0 -15
  124. package/src/server/ws-router-observability.test.ts +1 -1
  125. package/src/server/ws-router-settings.test.ts +2 -2
  126. package/src/server/ws-router.test.ts +2 -2
  127. package/src/server/ws-router.ts +0 -8
  128. package/src/shared/app-settings-types.ts +9 -20
  129. package/src/shared/claude-api-error-classification.ts +1 -1
  130. package/src/shared/protocol.ts +1 -11
  131. package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
  132. package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
  133. package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
  134. package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
  135. package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
  136. package/dist/client/assets/index-C-UYaDk6.css +0 -1
  137. package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
  138. package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
  139. package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
  140. package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
  141. package/src/server/agent.pty-rotation.test.ts +0 -322
  142. package/src/server/claude-pty/auth.test.ts +0 -38
  143. package/src/server/claude-pty/auth.ts +0 -17
  144. package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
  145. package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
  146. package/src/server/claude-pty/driver.test.ts +0 -1153
  147. package/src/server/claude-pty/driver.ts +0 -829
  148. package/src/server/claude-pty/env.test.ts +0 -79
  149. package/src/server/claude-pty/env.ts +0 -20
  150. package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
  151. package/src/server/claude-pty/jsonl-to-event.ts +0 -357
  152. package/src/server/claude-pty/parity-matrix.test.ts +0 -437
  153. package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
  154. package/src/server/claude-pty/pid-registry.test.ts +0 -178
  155. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
  156. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
  157. package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
  158. package/src/server/claude-pty/pty-instance-registry.ts +0 -172
  159. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
  160. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
  161. package/src/server/claude-pty/pty-process.adapter.ts +0 -60
  162. package/src/server/claude-pty/pty-process.test.ts +0 -49
  163. package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
  164. package/src/server/claude-pty/resolve-binary.test.ts +0 -118
  165. package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
  166. package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
  167. package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
  168. package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
  169. package/src/server/claude-pty/sandbox/platform.ts +0 -27
  170. package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
  171. package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
  172. package/src/server/claude-pty/sandbox/preflight.ts +0 -44
  173. package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
  174. package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
  175. package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
  176. package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
  177. package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
  178. package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
  179. package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
  180. package/src/server/claude-pty/settings-writer.test.ts +0 -22
  181. package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
  182. package/src/server/claude-pty/smoke-test.test.ts +0 -199
  183. package/src/server/claude-pty/smoke-test.ts +0 -182
  184. package/src/server/claude-pty/tui-control.test.ts +0 -300
  185. package/src/server/claude-pty/tui-control.ts +0 -174
  186. package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
  187. package/src/server/claude-pty/tui-source.adapter.ts +0 -294
  188. package/src/server/http-server.adapter.test.ts +0 -18
  189. package/src/server/http-server.adapter.ts +0 -39
  190. package/src/server/kanna-mcp-http.test.ts +0 -274
  191. package/src/server/kanna-mcp-http.ts +0 -194
  192. package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
  193. package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
  194. package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
  195. package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
  196. package/src/server/mcp-zod-compat.adapter.ts +0 -20
  197. package/src/shared/pty-instance.ts +0 -43
  198. /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
  199. /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
@@ -1,103 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import process from "node:process"
3
- import {
4
- collectTreePids,
5
- parsePsOutput,
6
- sampleProcessTreeUsage,
7
- sumTreeUsage,
8
- type PsProcessRow,
9
- } from "./pty-memory-sampler.adapter"
10
-
11
- describe("parsePsOutput", () => {
12
- test("parses normal ps output with pid/ppid/rss/pcpu", () => {
13
- const out = [
14
- " 1 0 1024 0.0",
15
- " 100 1 2048 5.2",
16
- " 101 100 4096 12.5",
17
- "",
18
- " 102 100 8192 99.9",
19
- ].join("\n")
20
- expect(parsePsOutput(out)).toEqual([
21
- { pid: 1, ppid: 0, rssKb: 1024, cpuPercent: 0.0 },
22
- { pid: 100, ppid: 1, rssKb: 2048, cpuPercent: 5.2 },
23
- { pid: 101, ppid: 100, rssKb: 4096, cpuPercent: 12.5 },
24
- { pid: 102, ppid: 100, rssKb: 8192, cpuPercent: 99.9 },
25
- ])
26
- })
27
-
28
- test("skips malformed rows without throwing", () => {
29
- const out = [
30
- "100 1 1024 3.0",
31
- "garbage line here",
32
- "abc def ghi jkl",
33
- "200 1 2048 7.5",
34
- " ",
35
- "300 1 100",
36
- ].join("\n")
37
- expect(parsePsOutput(out)).toEqual([
38
- { pid: 100, ppid: 1, rssKb: 1024, cpuPercent: 3.0 },
39
- { pid: 200, ppid: 1, rssKb: 2048, cpuPercent: 7.5 },
40
- ])
41
- })
42
-
43
- test("returns empty array for empty input", () => {
44
- expect(parsePsOutput("")).toEqual([])
45
- expect(parsePsOutput("\n\n\n")).toEqual([])
46
- })
47
- })
48
-
49
- describe("collectTreePids", () => {
50
- const rows: PsProcessRow[] = [
51
- { pid: 1, ppid: 0, rssKb: 0, cpuPercent: 0 },
52
- { pid: 100, ppid: 1, rssKb: 0, cpuPercent: 0 },
53
- { pid: 101, ppid: 100, rssKb: 0, cpuPercent: 0 },
54
- { pid: 102, ppid: 100, rssKb: 0, cpuPercent: 0 },
55
- { pid: 103, ppid: 101, rssKb: 0, cpuPercent: 0 },
56
- { pid: 200, ppid: 1, rssKb: 0, cpuPercent: 0 },
57
- ]
58
-
59
- test("collects root + descendants transitively", () => {
60
- expect(collectTreePids(rows, 100)).toEqual(new Set([100, 101, 102, 103]))
61
- })
62
-
63
- test("returns only root when no children", () => {
64
- expect(collectTreePids(rows, 200)).toEqual(new Set([200]))
65
- })
66
-
67
- test("returns only root when root absent from rows", () => {
68
- expect(collectTreePids(rows, 999)).toEqual(new Set([999]))
69
- })
70
- })
71
-
72
- describe("sumTreeUsage", () => {
73
- const rows: PsProcessRow[] = [
74
- { pid: 1, ppid: 0, rssKb: 100, cpuPercent: 1.0 },
75
- { pid: 100, ppid: 1, rssKb: 200, cpuPercent: 25.0 },
76
- { pid: 101, ppid: 100, rssKb: 300, cpuPercent: 50.5 },
77
- ]
78
-
79
- test("sums rss (kb->bytes) + cpu for pids in tree", () => {
80
- const tree = new Set<number>([100, 101])
81
- expect(sumTreeUsage(rows, tree)).toEqual({
82
- rssBytes: (200 + 300) * 1024,
83
- cpuPercent: 25.0 + 50.5,
84
- })
85
- })
86
-
87
- test("returns zeros for empty tree", () => {
88
- expect(sumTreeUsage(rows, new Set())).toEqual({ rssBytes: 0, cpuPercent: 0 })
89
- })
90
- })
91
-
92
- describe("sampleProcessTreeUsage", () => {
93
- test("returns positive rss + finite cpu for current process", async () => {
94
- const sample = await sampleProcessTreeUsage(process.pid)
95
- expect(sample).not.toBeNull()
96
- expect(sample?.rssBytes).toBeGreaterThan(0)
97
- expect(Number.isFinite(sample?.cpuPercent ?? Number.NaN)).toBe(true)
98
- }, 5_000)
99
-
100
- test("returns null for pid that does not exist", async () => {
101
- expect(await sampleProcessTreeUsage(2_147_483_646)).toBeNull()
102
- }, 5_000)
103
- })
@@ -1,86 +0,0 @@
1
- import { execFile } from "node:child_process"
2
- import { promisify } from "node:util"
3
-
4
- const execFileAsync = promisify(execFile)
5
-
6
- const PS_TIMEOUT_MS = 2000
7
-
8
- export interface PsProcessRow {
9
- pid: number
10
- ppid: number
11
- rssKb: number
12
- cpuPercent: number
13
- }
14
-
15
- export interface ProcessTreeSample {
16
- rssBytes: number
17
- cpuPercent: number
18
- }
19
-
20
- export function parsePsOutput(stdout: string): PsProcessRow[] {
21
- const rows: PsProcessRow[] = []
22
- for (const rawLine of stdout.split("\n")) {
23
- const line = rawLine.trim()
24
- if (!line) continue
25
- const parts = line.split(/\s+/)
26
- if (parts.length < 4) continue
27
- const pid = Number(parts[0])
28
- const ppid = Number(parts[1])
29
- const rssKb = Number(parts[2])
30
- const cpuPercent = Number(parts[3])
31
- if (!Number.isFinite(pid) || !Number.isFinite(ppid) || !Number.isFinite(rssKb) || !Number.isFinite(cpuPercent)) continue
32
- rows.push({ pid, ppid, rssKb, cpuPercent })
33
- }
34
- return rows
35
- }
36
-
37
- export function collectTreePids(rows: readonly PsProcessRow[], rootPid: number): Set<number> {
38
- const childrenByParent = new Map<number, number[]>()
39
- for (const row of rows) {
40
- const list = childrenByParent.get(row.ppid)
41
- if (list) list.push(row.pid)
42
- else childrenByParent.set(row.ppid, [row.pid])
43
- }
44
- const tree = new Set<number>([rootPid])
45
- const queue: number[] = [rootPid]
46
- while (queue.length > 0) {
47
- const next = queue.shift()
48
- if (next === undefined) break
49
- const kids = childrenByParent.get(next)
50
- if (!kids) continue
51
- for (const kid of kids) {
52
- if (tree.has(kid)) continue
53
- tree.add(kid)
54
- queue.push(kid)
55
- }
56
- }
57
- return tree
58
- }
59
-
60
- export function sumTreeUsage(rows: readonly PsProcessRow[], tree: ReadonlySet<number>): ProcessTreeSample {
61
- let totalKb = 0
62
- let totalCpu = 0
63
- for (const row of rows) {
64
- if (!tree.has(row.pid)) continue
65
- totalKb += row.rssKb
66
- totalCpu += row.cpuPercent
67
- }
68
- return { rssBytes: totalKb * 1024, cpuPercent: totalCpu }
69
- }
70
-
71
- export async function sampleProcessTreeUsage(rootPid: number): Promise<ProcessTreeSample | null> {
72
- let stdout: string
73
- try {
74
- const result = await execFileAsync("ps", ["-A", "-o", "pid=,ppid=,rss=,pcpu="], {
75
- timeout: PS_TIMEOUT_MS,
76
- maxBuffer: 4 * 1024 * 1024,
77
- })
78
- stdout = result.stdout
79
- } catch {
80
- return null
81
- }
82
- const rows = parsePsOutput(stdout)
83
- if (!rows.some((r) => r.pid === rootPid)) return null
84
- const tree = collectTreePids(rows, rootPid)
85
- return sumTreeUsage(rows, tree)
86
- }
@@ -1,60 +0,0 @@
1
- export interface PtyProcess {
2
- pid: number
3
- sendInput(data: string): Promise<void>
4
- resize(cols: number, rows: number): void
5
- exited: Promise<number>
6
- close(): void
7
- kill(signal?: NodeJS.Signals | number): void
8
- }
9
-
10
- export interface SpawnPtyProcessArgs {
11
- command: string
12
- args: string[]
13
- cwd: string
14
- env: NodeJS.ProcessEnv
15
- cols?: number
16
- rows?: number
17
- onOutput?: (chunk: string) => void
18
- }
19
-
20
- export async function spawnPtyProcess(opts: SpawnPtyProcessArgs): Promise<PtyProcess> {
21
- if (typeof Bun.Terminal !== "function") {
22
- throw new Error("Bun.Terminal not available — requires Bun 1.3.5+")
23
- }
24
-
25
- const cols = opts.cols ?? 120
26
- const rows = opts.rows ?? 40
27
-
28
- const terminal = new Bun.Terminal({
29
- cols,
30
- rows,
31
- name: "xterm-256color",
32
- data: (_t, data) => {
33
- if (opts.onOutput) {
34
- const chunk = Buffer.from(data).toString("utf8")
35
- opts.onOutput(chunk)
36
- }
37
- },
38
- })
39
-
40
- const proc = Bun.spawn([opts.command, ...opts.args], {
41
- cwd: opts.cwd,
42
- env: opts.env,
43
- terminal,
44
- })
45
-
46
- return {
47
- pid: proc.pid,
48
- async sendInput(data) { terminal.write(data) },
49
- resize(newCols, newRows) { terminal.resize(newCols, newRows) },
50
- exited: proc.exited,
51
- close() {
52
- try { terminal.close() } catch { }
53
- try { proc.kill("SIGTERM") } catch { }
54
- },
55
- kill(signal) {
56
- try { terminal.close() } catch { }
57
- try { proc.kill(signal ?? "SIGKILL") } catch { }
58
- },
59
- }
60
- }
@@ -1,49 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { spawnPtyProcess } from "./pty-process.adapter"
3
-
4
- describe("spawnPtyProcess", () => {
5
- test(
6
- "spawns a child process and exits cleanly",
7
- async () => {
8
- if (process.platform === "win32") {
9
- console.log("skip: PTY not supported on Windows")
10
- return
11
- }
12
- if (typeof Bun.Terminal !== "function") {
13
- console.log("skip: Bun.Terminal not available")
14
- return
15
- }
16
- const handle = await spawnPtyProcess({
17
- command: "/bin/sh",
18
- args: ["-c", "echo hello"],
19
- cwd: "/tmp",
20
- env: process.env,
21
- cols: 80,
22
- rows: 24,
23
- })
24
- const exitCode = await handle.exited
25
- expect(exitCode).toBe(0)
26
- handle.close()
27
- },
28
- 30_000,
29
- )
30
-
31
- test(
32
- "captures output via onOutput callback",
33
- async () => {
34
- if (process.platform === "win32" || typeof Bun.Terminal !== "function") return
35
- const chunks: string[] = []
36
- const handle = await spawnPtyProcess({
37
- command: "/bin/sh",
38
- args: ["-c", "echo hi"],
39
- cwd: "/tmp",
40
- env: process.env,
41
- onOutput: (chunk) => chunks.push(chunk),
42
- })
43
- await handle.exited
44
- handle.close()
45
- expect(chunks.join("")).toContain("hi")
46
- },
47
- 30_000,
48
- )
49
- })
@@ -1,106 +0,0 @@
1
- import { stat } from "node:fs/promises"
2
- import path from "node:path"
3
- import { promisify } from "node:util"
4
- import { execFile } from "node:child_process"
5
-
6
- const execFileAsync = promisify(execFile)
7
-
8
- export interface ResolveClaudeBinaryArgs {
9
- env: NodeJS.ProcessEnv
10
- homeDir: string
11
- cwd?: string
12
- platform?: NodeJS.Platform
13
- arch?: NodeJS.Architecture
14
- }
15
-
16
- export interface ResolveClaudeBinaryResult {
17
- path: string
18
- source: "env-CLAUDE_EXECUTABLE" | "env-CLAUDE_CODE_EXECPATH" | "PATH" | "node_modules"
19
- triedPaths: string[]
20
- }
21
-
22
- function expandTilde(p: string, home: string): string {
23
- return p.replace(/^~(?=\/|$)/, home)
24
- }
25
-
26
- async function isExecutableFile(p: string): Promise<boolean> {
27
- try {
28
- const s = await stat(p)
29
- return s.isFile()
30
- } catch {
31
- return false
32
- }
33
- }
34
-
35
- async function whichClaude(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): Promise<string | null> {
36
- const cmd = platform === "win32" ? "where" : "which"
37
- try {
38
- const { stdout } = await execFileAsync(cmd, ["claude"], { env, timeout: 2000 })
39
- const first = stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0)
40
- if (!first) return null
41
- if (!(await isExecutableFile(first))) return null
42
- return first
43
- } catch {
44
- return null
45
- }
46
- }
47
-
48
- function buildNodeModulesCandidates(cwd: string, platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
49
- const tag = `${platform}-${arch}`
50
- const pkgDir = `@anthropic-ai/claude-agent-sdk-${tag}`
51
- const candidates: string[] = []
52
- let dir = path.resolve(cwd)
53
- while (true) {
54
- candidates.push(path.join(dir, "node_modules", pkgDir, "claude"))
55
- const parent = path.dirname(dir)
56
- if (parent === dir) break
57
- dir = parent
58
- }
59
- return candidates
60
- }
61
-
62
- export async function resolveClaudeBinary(args: ResolveClaudeBinaryArgs): Promise<ResolveClaudeBinaryResult> {
63
- const env = args.env
64
- const home = args.homeDir
65
- const cwd = args.cwd ?? process.cwd()
66
- const platform = args.platform ?? process.platform
67
- const arch = args.arch ?? process.arch
68
- const tried: string[] = []
69
-
70
- if (env.CLAUDE_EXECUTABLE) {
71
- const candidate = expandTilde(env.CLAUDE_EXECUTABLE, home)
72
- tried.push(`CLAUDE_EXECUTABLE=${candidate}`)
73
- if (await isExecutableFile(candidate)) {
74
- return { path: candidate, source: "env-CLAUDE_EXECUTABLE", triedPaths: tried }
75
- }
76
- }
77
-
78
- if (env.CLAUDE_CODE_EXECPATH) {
79
- const candidate = expandTilde(env.CLAUDE_CODE_EXECPATH, home)
80
- tried.push(`CLAUDE_CODE_EXECPATH=${candidate}`)
81
- if (await isExecutableFile(candidate)) {
82
- return { path: candidate, source: "env-CLAUDE_CODE_EXECPATH", triedPaths: tried }
83
- }
84
- }
85
-
86
- const fromPath = await whichClaude(env, platform)
87
- tried.push(`PATH lookup: ${fromPath ?? "<not found>"}`)
88
- if (fromPath) {
89
- return { path: fromPath, source: "PATH", triedPaths: tried }
90
- }
91
-
92
- const candidates = buildNodeModulesCandidates(cwd, platform, arch)
93
- for (const candidate of candidates) {
94
- tried.push(candidate)
95
- if (await isExecutableFile(candidate)) {
96
- return { path: candidate, source: "node_modules", triedPaths: tried }
97
- }
98
- }
99
-
100
- throw new Error(
101
- `Unable to locate the \`claude\` CLI binary. Set CLAUDE_EXECUTABLE to an absolute path, `
102
- + `install \`@anthropic-ai/claude-code\` globally so \`claude\` is on PATH, or install `
103
- + `\`@anthropic-ai/claude-agent-sdk\` so the platform-bundled binary exists in node_modules. `
104
- + `Tried:\n - ${tried.join("\n - ")}`,
105
- )
106
- }
@@ -1,118 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { resolveClaudeBinary } from "./resolve-binary.adapter"
6
-
7
- describe("resolveClaudeBinary", () => {
8
- let workDir: string
9
-
10
- beforeEach(async () => {
11
- workDir = await mkdtemp(path.join(tmpdir(), "kanna-resolve-binary-"))
12
- })
13
-
14
- afterEach(async () => {
15
- await rm(workDir, { recursive: true, force: true })
16
- })
17
-
18
- async function makeExec(p: string): Promise<void> {
19
- await mkdir(path.dirname(p), { recursive: true })
20
- await writeFile(p, "#!/bin/sh\necho fake\n", { encoding: "utf8" })
21
- await chmod(p, 0o755)
22
- }
23
-
24
- test("returns CLAUDE_EXECUTABLE when set and exists", async () => {
25
- const bin = path.join(workDir, "fake-claude")
26
- await makeExec(bin)
27
- const result = await resolveClaudeBinary({
28
- env: { CLAUDE_EXECUTABLE: bin, PATH: "" },
29
- homeDir: workDir,
30
- cwd: workDir,
31
- platform: "darwin",
32
- arch: "arm64",
33
- })
34
- expect(result.source).toBe("env-CLAUDE_EXECUTABLE")
35
- expect(result.path).toBe(bin)
36
- })
37
-
38
- test("expands tilde in CLAUDE_EXECUTABLE", async () => {
39
- const bin = path.join(workDir, "tilde-claude")
40
- await makeExec(bin)
41
- const result = await resolveClaudeBinary({
42
- env: { CLAUDE_EXECUTABLE: "~/tilde-claude", PATH: "" },
43
- homeDir: workDir,
44
- cwd: workDir,
45
- platform: "darwin",
46
- arch: "arm64",
47
- })
48
- expect(result.path).toBe(bin)
49
- })
50
-
51
- test("falls back to CLAUDE_CODE_EXECPATH when CLAUDE_EXECUTABLE missing file", async () => {
52
- const bin = path.join(workDir, "execpath-claude")
53
- await makeExec(bin)
54
- const result = await resolveClaudeBinary({
55
- env: {
56
- CLAUDE_EXECUTABLE: path.join(workDir, "does-not-exist"),
57
- CLAUDE_CODE_EXECPATH: bin,
58
- PATH: "",
59
- },
60
- homeDir: workDir,
61
- cwd: workDir,
62
- platform: "darwin",
63
- arch: "arm64",
64
- })
65
- expect(result.source).toBe("env-CLAUDE_CODE_EXECPATH")
66
- expect(result.path).toBe(bin)
67
- })
68
-
69
- test("falls back to node_modules platform-bundled binary", async () => {
70
- const inner = path.join(workDir, "nested", "deep")
71
- await mkdir(inner, { recursive: true })
72
- const bundled = path.join(workDir, "node_modules", "@anthropic-ai", "claude-agent-sdk-darwin-arm64", "claude")
73
- await makeExec(bundled)
74
- const result = await resolveClaudeBinary({
75
- env: { PATH: "" },
76
- homeDir: workDir,
77
- cwd: inner,
78
- platform: "darwin",
79
- arch: "arm64",
80
- })
81
- expect(result.source).toBe("node_modules")
82
- expect(result.path).toBe(bundled)
83
- })
84
-
85
- test("walks parent dirs when looking up node_modules bundled binary", async () => {
86
- const inner = path.join(workDir, "a", "b", "c")
87
- await mkdir(inner, { recursive: true })
88
- const bundled = path.join(workDir, "node_modules", "@anthropic-ai", "claude-agent-sdk-linux-x64", "claude")
89
- await makeExec(bundled)
90
- const result = await resolveClaudeBinary({
91
- env: { PATH: "" },
92
- homeDir: workDir,
93
- cwd: inner,
94
- platform: "linux",
95
- arch: "x64",
96
- })
97
- expect(result.path).toBe(bundled)
98
- })
99
-
100
- test("throws with all tried paths when nothing resolves", async () => {
101
- let err: unknown
102
- try {
103
- await resolveClaudeBinary({
104
- env: { CLAUDE_EXECUTABLE: path.join(workDir, "missing"), PATH: "" },
105
- homeDir: workDir,
106
- cwd: workDir,
107
- platform: "darwin",
108
- arch: "arm64",
109
- })
110
- } catch (caught) {
111
- err = caught
112
- }
113
- expect(err).toBeInstanceOf(Error)
114
- expect((err as Error).message).toContain("Unable to locate")
115
- expect((err as Error).message).toContain("CLAUDE_EXECUTABLE")
116
- expect((err as Error).message).toContain("node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64/claude")
117
- }, 10_000)
118
- })
@@ -1,19 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises"
2
- import path from "node:path"
3
- import { tmpdir } from "node:os"
4
-
5
- export async function createRuntimeDir(prefix: string): Promise<string> {
6
- return await mkdtemp(path.join(tmpdir(), prefix))
7
- }
8
-
9
- export async function writeRuntimeFile(
10
- filePath: string,
11
- contents: string,
12
- options?: { encoding?: BufferEncoding; mode?: number },
13
- ): Promise<void> {
14
- await writeFile(filePath, contents, options)
15
- }
16
-
17
- export async function removeRuntimeDir(dir: string): Promise<void> {
18
- await rm(dir, { recursive: true, force: true })
19
- }
@@ -1,17 +0,0 @@
1
- import { spawn } from "node:child_process"
2
-
3
- let cached: boolean | null = null
4
-
5
- export async function detectBwrap(): Promise<boolean> {
6
- if (cached !== null) return cached
7
- cached = await new Promise<boolean>((resolve) => {
8
- const child = spawn("/usr/bin/which", ["bwrap"], { stdio: ["ignore", "ignore", "ignore"] })
9
- child.on("close", (code) => resolve(code === 0))
10
- child.on("error", () => resolve(false))
11
- })
12
- return cached
13
- }
14
-
15
- export function resetBwrapCacheForTest(): void {
16
- cached = null
17
- }
@@ -1,17 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { detectBwrap, resetBwrapCacheForTest } from "./detect.adapter"
3
-
4
- describe("detectBwrap", () => {
5
- test("returns boolean (real platform check)", async () => {
6
- resetBwrapCacheForTest()
7
- const result = await detectBwrap()
8
- expect(typeof result).toBe("boolean")
9
- })
10
-
11
- test("subsequent calls hit cache (same result)", async () => {
12
- resetBwrapCacheForTest()
13
- const first = await detectBwrap()
14
- const second = await detectBwrap()
15
- expect(second).toBe(first)
16
- })
17
- })
@@ -1,35 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { isSandboxSupported, isSandboxEnabledAsync } from "./platform"
3
- import { resetBwrapCacheForTest } from "./detect.adapter"
4
-
5
- describe("isSandboxSupported", () => {
6
- test("true on darwin", () => {
7
- expect(isSandboxSupported("darwin")).toBe(true)
8
- })
9
- test("false on linux (P4.1)", () => {
10
- expect(isSandboxSupported("linux")).toBe(false)
11
- })
12
- test("false on win32", () => {
13
- expect(isSandboxSupported("win32")).toBe(false)
14
- })
15
- })
16
-
17
- describe("isSandboxEnabledAsync", () => {
18
- test("respects env=off on linux", async () => {
19
- expect(await isSandboxEnabledAsync({ platform: "linux", env: "off" })).toBe(false)
20
- })
21
-
22
- test("linux: depends on bwrap detection (sync no, async maybe yes)", async () => {
23
- resetBwrapCacheForTest()
24
- const r = await isSandboxEnabledAsync({ platform: "linux", env: undefined })
25
- expect(typeof r).toBe("boolean")
26
- })
27
-
28
- test("darwin: always enabled when env not 'off'", async () => {
29
- expect(await isSandboxEnabledAsync({ platform: "darwin", env: undefined })).toBe(true)
30
- })
31
-
32
- test("win32: always false", async () => {
33
- expect(await isSandboxEnabledAsync({ platform: "win32", env: "on" })).toBe(false)
34
- })
35
- })
@@ -1,27 +0,0 @@
1
- import { detectBwrap } from "./detect.adapter"
2
- import { log } from "../../../shared/log"
3
-
4
- export function isSandboxSupported(platform: NodeJS.Platform): boolean {
5
- return platform === "darwin"
6
- }
7
-
8
- export async function isSandboxEnabledAsync(args: {
9
- platform: NodeJS.Platform
10
- env: string | undefined
11
- }): Promise<boolean> {
12
- if (args.env === "off") return false
13
- if (args.platform === "darwin") return true
14
- if (args.platform === "linux") {
15
- const ok = await detectBwrap()
16
- if (!ok) {
17
- log.warn(
18
- "[claude-pty/sandbox] bwrap not found on PATH — PTY OS sandbox is "
19
- + "DISABLED (loses defense-in-depth against built-in credential "
20
- + "reads). Install bubblewrap (apt/dnf/pacman install bubblewrap) "
21
- + "or set KANNA_PTY_SANDBOX=off to silence this warning.",
22
- )
23
- }
24
- return ok
25
- }
26
- return false
27
- }
@@ -1,14 +0,0 @@
1
- import { spawn } from "node:child_process"
2
- import { writeFile } from "node:fs/promises"
3
-
4
- export function spawnExitCode(command: string, args: string[]): Promise<number> {
5
- return new Promise<number>((resolve) => {
6
- const child = spawn(command, args, { stdio: ["ignore", "ignore", "ignore"] })
7
- child.on("close", (code) => resolve(code ?? -1))
8
- child.on("error", () => resolve(-1))
9
- })
10
- }
11
-
12
- export function writeTextFile(p: string, contents: string): Promise<void> {
13
- return writeFile(p, contents, "utf8")
14
- }