@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,294 +0,0 @@
1
- import { readdir, stat, open } from "node:fs/promises"
2
- import { existsSync } from "node:fs"
3
- import path from "node:path"
4
- import { awaitClaudeSessionForPid } from "./claude-session-registry.adapter"
5
- import { computeJsonlPath } from "./jsonl-path.adapter"
6
-
7
- export async function findLatestTranscript(
8
- projectDir: string,
9
- opts: { minMtimeMs?: number } = {},
10
- ): Promise<string | null> {
11
- if (!existsSync(projectDir)) return null
12
- let entries: string[]
13
- try {
14
- entries = await readdir(projectDir)
15
- } catch {
16
- return null
17
- }
18
- const jsonlNames = entries.filter((n) => n.endsWith(".jsonl"))
19
- if (jsonlNames.length === 0) return null
20
- const floor = opts.minMtimeMs ?? 0
21
- let bestPath: string | null = null
22
- let bestMtime = 0
23
- for (const name of jsonlNames) {
24
- const full = path.join(projectDir, name)
25
- try {
26
- const s = await stat(full)
27
- if (s.mtimeMs < floor) continue
28
- if (s.mtimeMs > bestMtime) {
29
- bestMtime = s.mtimeMs
30
- bestPath = full
31
- }
32
- } catch {
33
- }
34
- }
35
- return bestPath
36
- }
37
-
38
- export interface TranscriptStream {
39
- lines: AsyncIterable<string>
40
- filePath: Promise<string>
41
- close(): void
42
- }
43
-
44
- export interface StartTranscriptStreamArgs {
45
- projectDir: string
46
- knownFilePath?: string
47
- minMtimeMs?: number
48
- pollIntervalMs?: number
49
- firstFileTimeoutMs?: number
50
- claudeChildPid?: number
51
- homeDir?: string
52
- sessionRegistryTimeoutMs?: number
53
- sessionRegistryPollMs?: number
54
- }
55
-
56
- const DEFAULT_FIRST_FILE_TIMEOUT_MS = 20_000
57
- const DEFAULT_POLL_INTERVAL_MS = 50
58
- const DEFAULT_SESSION_REGISTRY_TIMEOUT_MS = 2_000
59
- const DEFAULT_SESSION_REGISTRY_POLL_MS = 20
60
-
61
- export async function startTranscriptStream(args: StartTranscriptStreamArgs): Promise<TranscriptStream> {
62
- const lineQueue: string[] = []
63
- const lineWaiters: Array<(r: IteratorResult<string, undefined>) => void> = []
64
- let buffer = ""
65
- let position = 0
66
- let closed = false
67
- let pollTimer: ReturnType<typeof setInterval> | null = null
68
-
69
- function pushLine(line: string) {
70
- const w = lineWaiters.shift()
71
- if (w) w({ value: line, done: false })
72
- else lineQueue.push(line)
73
- }
74
-
75
- function endLines() {
76
- const done: IteratorReturnResult<undefined> = { value: undefined, done: true }
77
- while (lineWaiters.length > 0) {
78
- const w = lineWaiters.shift()
79
- if (w) w(done)
80
- }
81
- }
82
-
83
- async function readNewBytes(filePath: string) {
84
- try {
85
- const s = await stat(filePath)
86
- if (s.size <= position) return
87
- const fd = await open(filePath, "r")
88
- try {
89
- const length = s.size - position
90
- const buf = Buffer.alloc(length)
91
- await fd.read(buf, 0, length, position)
92
- position = s.size
93
- buffer += buf.toString("utf8")
94
- const parts = buffer.split("\n")
95
- buffer = parts.pop() ?? ""
96
- for (const line of parts) {
97
- if (line.length === 0) continue
98
- pushLine(line)
99
- }
100
- } finally {
101
- await fd.close()
102
- }
103
- } catch {
104
- }
105
- }
106
-
107
- function startFollowing(filePath: string) {
108
- const interval = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
109
- pollTimer = setInterval(() => { void readNewBytes(filePath) }, interval)
110
- void readNewBytes(filePath)
111
- }
112
-
113
- async function locateFirstFile(): Promise<string> {
114
- if (args.knownFilePath) return args.knownFilePath
115
- if (args.claudeChildPid && args.homeDir) {
116
- const entry = await awaitClaudeSessionForPid({
117
- homeDir: args.homeDir,
118
- pid: args.claudeChildPid,
119
- timeoutMs: args.sessionRegistryTimeoutMs ?? DEFAULT_SESSION_REGISTRY_TIMEOUT_MS,
120
- pollIntervalMs: args.sessionRegistryPollMs ?? DEFAULT_SESSION_REGISTRY_POLL_MS,
121
- })
122
- if (entry) {
123
- const computed = computeJsonlPath({
124
- homeDir: args.homeDir,
125
- cwd: entry.cwd,
126
- sessionId: entry.sessionId,
127
- })
128
- const jsonlPollMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
129
- const jsonlTimeoutMs = args.firstFileTimeoutMs ?? DEFAULT_FIRST_FILE_TIMEOUT_MS
130
- const jsonlPollStart = Date.now()
131
- while (!closed) {
132
- if (existsSync(computed)) return computed
133
- if (Date.now() - jsonlPollStart > jsonlTimeoutMs) {
134
- throw new Error(
135
- `registry-resolved JSONL ${computed} did not appear in ${jsonlTimeoutMs}ms (claude TUI likely missed first prompt)`,
136
- )
137
- }
138
- await new Promise((r) => setTimeout(r, jsonlPollMs))
139
- }
140
- throw new Error("transcript stream closed before registry-resolved JSONL appeared")
141
- }
142
- }
143
- const timeoutMs = args.firstFileTimeoutMs ?? DEFAULT_FIRST_FILE_TIMEOUT_MS
144
- const findOpts = { minMtimeMs: args.minMtimeMs }
145
- const existing = await findLatestTranscript(args.projectDir, findOpts)
146
- if (existing) return existing
147
- return new Promise<string>((resolve, reject) => {
148
- const start = Date.now()
149
- const pollMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
150
- const timer = setInterval(async () => {
151
- if (closed) {
152
- clearInterval(timer)
153
- reject(new Error("transcript stream closed before first file appeared"))
154
- return
155
- }
156
- if (Date.now() - start > timeoutMs) {
157
- clearInterval(timer)
158
- reject(new Error(`transcript file did not appear in ${timeoutMs}ms under ${args.projectDir}`))
159
- return
160
- }
161
- const found = await findLatestTranscript(args.projectDir, findOpts)
162
- if (found) {
163
- clearInterval(timer)
164
- resolve(found)
165
- }
166
- }, pollMs)
167
- })
168
- }
169
-
170
- const filePathPromise = locateFirstFile()
171
- void filePathPromise
172
- .then((fp) => { if (!closed) startFollowing(fp) })
173
- .catch(() => {
174
- })
175
-
176
- const lines: AsyncIterable<string> = {
177
- [Symbol.asyncIterator]() {
178
- return {
179
- next(): Promise<IteratorResult<string, undefined>> {
180
- if (lineQueue.length > 0) {
181
- const v = lineQueue.shift()
182
- if (v !== undefined) return Promise.resolve({ value: v, done: false })
183
- }
184
- if (closed) return Promise.resolve({ value: undefined, done: true as const })
185
- return new Promise((resolve) => lineWaiters.push(resolve))
186
- },
187
- }
188
- },
189
- }
190
-
191
- return {
192
- lines,
193
- filePath: filePathPromise,
194
- close() {
195
- if (closed) return
196
- closed = true
197
- if (pollTimer) clearInterval(pollTimer)
198
- endLines()
199
- },
200
- }
201
- }
202
-
203
- export async function waitForResultEntry(
204
- stream: TranscriptStream,
205
- opts: { timeoutMs?: number; signal?: AbortSignal } = {},
206
- ): Promise<{ rawLine: string; parsed: { type: string } }> {
207
- const timeoutMs = opts.timeoutMs
208
- return new Promise((resolve, reject) => {
209
- let settled = false
210
-
211
- const timer = timeoutMs !== undefined
212
- ? setTimeout(() => {
213
- if (settled) return
214
- settled = true
215
- stream.close()
216
- reject(new Error(`waitForResultEntry timed out after ${timeoutMs}ms`))
217
- }, timeoutMs)
218
- : null
219
-
220
- const onAbort = () => {
221
- if (settled) return
222
- settled = true
223
- if (timer) clearTimeout(timer)
224
- stream.close()
225
- reject(new Error("aborted"))
226
- }
227
-
228
- if (opts.signal) {
229
- if (opts.signal.aborted) {
230
- onAbort()
231
- return
232
- }
233
- opts.signal.addEventListener("abort", onAbort, { once: true })
234
- }
235
-
236
- async function consume() {
237
- try {
238
- for await (const line of stream.lines) {
239
- if (settled) return
240
- let parsed: {
241
- type?: string
242
- subtype?: string
243
- error?: string
244
- isApiErrorMessage?: boolean
245
- apiErrorStatus?: number
246
- isSidechain?: boolean
247
- message?: { stop_reason?: string | null }
248
- }
249
- try { parsed = JSON.parse(line) } catch { continue }
250
- const stopReason = parsed.message?.stop_reason
251
- const isTerminalAssistant =
252
- parsed.type === "assistant" &&
253
- parsed.isSidechain !== true &&
254
- typeof stopReason === "string" &&
255
- ["end_turn", "stop_sequence", "max_tokens", "refusal"].includes(stopReason)
256
- const isTurnEnd =
257
- parsed.type === "result" ||
258
- (parsed.type === "system" && parsed.subtype === "turn_duration") ||
259
- isTerminalAssistant
260
- if (isTurnEnd) {
261
- settled = true
262
- if (timer) clearTimeout(timer)
263
- if (opts.signal) opts.signal.removeEventListener("abort", onAbort)
264
- resolve({ rawLine: line, parsed: { type: parsed.type ?? "result" } })
265
- return
266
- }
267
- if (parsed.type === "assistant" && parsed.isApiErrorMessage && parsed.apiErrorStatus === 429) {
268
- settled = true
269
- if (timer) clearTimeout(timer)
270
- if (opts.signal) opts.signal.removeEventListener("abort", onAbort)
271
- const rlErr: Error & { code: string } = Object.assign(new Error("rate_limited"), { code: "rate_limited" })
272
- reject(rlErr)
273
- return
274
- }
275
- }
276
- if (!settled) {
277
- settled = true
278
- if (timer) clearTimeout(timer)
279
- if (opts.signal) opts.signal.removeEventListener("abort", onAbort)
280
- reject(new Error("transcript stream ended before result entry"))
281
- }
282
- } catch (err) {
283
- if (!settled) {
284
- settled = true
285
- if (timer) clearTimeout(timer)
286
- if (opts.signal) opts.signal.removeEventListener("abort", onAbort)
287
- reject(err)
288
- }
289
- }
290
- }
291
-
292
- void consume()
293
- })
294
- }
@@ -1,18 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { closeHttpServer, createHttpServer } from "./http-server.adapter"
3
-
4
- describe("createHttpServer", () => {
5
- test("disables socket reaping so long-lived SSE streams survive idle", async () => {
6
- const server = createHttpServer((_req, res) => {
7
- res.statusCode = 204
8
- res.end()
9
- })
10
- try {
11
- expect(server.requestTimeout).toBe(0)
12
- expect(server.timeout).toBe(0)
13
- expect(server.keepAliveTimeout).toBe(0)
14
- } finally {
15
- await closeHttpServer(server)
16
- }
17
- })
18
- })
@@ -1,39 +0,0 @@
1
- import http from "node:http"
2
- import type { AddressInfo } from "node:net"
3
-
4
- export type HttpRequestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => void
5
-
6
- export interface HttpServerHandle {
7
- port: number
8
- close: () => Promise<void>
9
- }
10
-
11
- export function createHttpServer(handler: HttpRequestHandler) {
12
- const server = http.createServer(handler)
13
- server.requestTimeout = 0
14
- server.timeout = 0
15
- server.keepAliveTimeout = 0
16
- return server
17
- }
18
-
19
- export function listen(server: http.Server, port: number, host: string): Promise<AddressInfo> {
20
- return new Promise<AddressInfo>((resolve, reject) => {
21
- server.once("error", reject)
22
- server.listen(port, host, () => {
23
- server.off("error", reject)
24
- const addr = server.address()
25
- if (!addr || typeof addr !== "object") { reject(new Error("server.address() is not an AddressInfo")); return }
26
- resolve(addr)
27
- })
28
- })
29
- }
30
-
31
- export function closeHttpServer(server: http.Server): Promise<void> {
32
- return new Promise<void>((resolve) => {
33
- server.close(() => resolve())
34
- })
35
- }
36
-
37
- export type HttpServer = http.Server
38
- export type HttpIncomingMessage = http.IncomingMessage
39
- export type HttpServerResponse = http.ServerResponse
@@ -1,274 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { startKannaMcpHttpServer, buildMcpConfigJson, buildChannelNotification } from "./kanna-mcp-http"
3
- import type { McpServerConfig } from "../shared/types"
4
-
5
- const baseArgs = {
6
- projectId: "proj-test",
7
- localPath: "/tmp",
8
- chatId: "chat-test",
9
- sessionId: "sess-test",
10
- tunnelGateway: null,
11
- }
12
-
13
- describe("startKannaMcpHttpServer", () => {
14
- test("binds loopback on ephemeral port and exposes a /mcp URL", async () => {
15
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
16
- try {
17
- expect(handle.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/mcp$/)
18
- expect(handle.bearerToken).toHaveLength(64)
19
- } finally {
20
- await handle.close()
21
- }
22
- })
23
-
24
- test("rejects requests without Authorization header (401)", async () => {
25
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
26
- try {
27
- const res = await fetch(handle.url, { method: "POST", body: "{}" })
28
- expect(res.status).toBe(401)
29
- } finally {
30
- await handle.close()
31
- }
32
- })
33
-
34
- test("rejects requests with mismatched Bearer token (401)", async () => {
35
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
36
- try {
37
- const res = await fetch(handle.url, {
38
- method: "POST",
39
- headers: { Authorization: "Bearer wrong-token" },
40
- body: "{}",
41
- })
42
- expect(res.status).toBe(401)
43
- } finally {
44
- await handle.close()
45
- }
46
- })
47
-
48
- test("rejects requests with non-Bearer scheme (401)", async () => {
49
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
50
- try {
51
- const res = await fetch(handle.url, {
52
- method: "POST",
53
- headers: { Authorization: `Basic ${handle.bearerToken}` },
54
- body: "{}",
55
- })
56
- expect(res.status).toBe(401)
57
- } finally {
58
- await handle.close()
59
- }
60
- })
61
-
62
- test("forwards requests past auth when Bearer matches", async () => {
63
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
64
- try {
65
- const res = await fetch(handle.url, {
66
- method: "POST",
67
- headers: {
68
- Authorization: `Bearer ${handle.bearerToken}`,
69
- "Content-Type": "application/json",
70
- Accept: "application/json, text/event-stream",
71
- },
72
- body: JSON.stringify({
73
- jsonrpc: "2.0",
74
- id: 1,
75
- method: "initialize",
76
- params: {
77
- protocolVersion: "2024-11-05",
78
- capabilities: {},
79
- clientInfo: { name: "kanna-pty-test", version: "0.0.0" },
80
- },
81
- }),
82
- })
83
- expect(res.status).not.toBe(401)
84
- } finally {
85
- await handle.close()
86
- }
87
- })
88
-
89
- test("close() shuts down the listener so subsequent requests fail", async () => {
90
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
91
- const url = handle.url
92
- await handle.close()
93
- await expect(
94
- fetch(url, {
95
- method: "POST",
96
- headers: { Authorization: `Bearer ${handle.bearerToken}` },
97
- body: "{}",
98
- }),
99
- ).rejects.toBeDefined()
100
- })
101
-
102
- test("close() is idempotent", async () => {
103
- const handle = await startKannaMcpHttpServer({ args: baseArgs })
104
- await handle.close()
105
- await handle.close()
106
- })
107
- })
108
-
109
- describe("buildChannelNotification", () => {
110
- test("builds a notifications/claude/channel payload with kanna source", () => {
111
- const n = buildChannelNotification("do the thing")
112
- expect(n.method).toBe("notifications/claude/channel")
113
- expect(n.params.content).toBe("do the thing")
114
- expect(n.params.meta).toEqual({ source: "kanna" })
115
- })
116
-
117
- test("merges extra meta but keeps source=kanna", () => {
118
- const n = buildChannelNotification("x", { eventType: "delegate" })
119
- expect(n.params.meta).toEqual({ source: "kanna", eventType: "delegate" })
120
- })
121
- })
122
-
123
- describe("buildMcpConfigJson", () => {
124
- test("encodes http MCP server config with Bearer header", () => {
125
- const json = buildMcpConfigJson({
126
- url: "http://127.0.0.1:55555/mcp",
127
- bearerToken: "abcdef0123456789",
128
- })
129
- const parsed = JSON.parse(json)
130
- expect(parsed.mcpServers.kanna.type).toBe("http")
131
- expect(parsed.mcpServers.kanna.url).toBe("http://127.0.0.1:55555/mcp")
132
- expect(parsed.mcpServers.kanna.headers.Authorization).toBe("Bearer abcdef0123456789")
133
- })
134
- })
135
-
136
- describe("startKannaMcpHttpServer channel surface", () => {
137
- const channelArgs = {
138
- projectId: "p1",
139
- localPath: "/tmp",
140
- chatId: "c1",
141
- sessionId: "s1",
142
- tunnelGateway: null,
143
- forceInteractiveToolCallbacks: true,
144
- } as unknown as Parameters<typeof startKannaMcpHttpServer>[0]["args"]
145
-
146
- test("handle exposes pushChannelPrompt + channelClientReady", async () => {
147
- const handle = await startKannaMcpHttpServer({ args: channelArgs, port: 0 })
148
- try {
149
- expect(typeof handle.pushChannelPrompt).toBe("function")
150
- expect(handle.channelClientReady).toBeInstanceOf(Promise)
151
- } finally {
152
- await handle.close()
153
- }
154
- })
155
-
156
- test("pushChannelPrompt does not throw before a client connects (no-op safe)", async () => {
157
- const handle = await startKannaMcpHttpServer({ args: channelArgs, port: 0 })
158
- try {
159
- await handle.pushChannelPrompt("hello")
160
- } finally {
161
- await handle.close()
162
- }
163
- })
164
- })
165
-
166
- const HANDLE = { url: "http://127.0.0.1:1234/mcp", bearerToken: "tok" }
167
-
168
- function stdio(name: string, command = "/bin/ls", enabled = true): McpServerConfig {
169
- return {
170
- id: name,
171
- name,
172
- enabled,
173
- createdAt: "", updatedAt: "",
174
- lastTest: { status: "untested" },
175
- transport: "stdio",
176
- command,
177
- args: ["-la"],
178
- env: { FOO: "bar" },
179
- }
180
- }
181
-
182
- describe("buildMcpConfigJson — user servers", () => {
183
- test("no user servers keeps just kanna", () => {
184
- const json = JSON.parse(buildMcpConfigJson(HANDLE))
185
- expect(Object.keys(json.mcpServers)).toEqual(["kanna"])
186
- })
187
-
188
- test("stdio user entry included with correct shape", () => {
189
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [stdio("fs")]))
190
- expect(json.mcpServers.fs).toEqual({
191
- type: "stdio",
192
- command: "/bin/ls",
193
- args: ["-la"],
194
- env: { FOO: "bar" },
195
- })
196
- })
197
-
198
- test("stdio with cwd includes cwd", () => {
199
- const cfg: McpServerConfig = { ...stdio("fs"), cwd: "/tmp/work" } as McpServerConfig
200
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [cfg]))
201
- expect(json.mcpServers.fs.cwd).toBe("/tmp/work")
202
- })
203
-
204
- test("disabled entries dropped", () => {
205
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [stdio("fs", "/bin/ls", false)]))
206
- expect(json.mcpServers.fs).toBeUndefined()
207
- })
208
-
209
- test("collision with KANNA_MCP_SERVER_NAME filtered", () => {
210
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [stdio("kanna")]))
211
- expect(Object.keys(json.mcpServers)).toEqual(["kanna"])
212
- expect(json.mcpServers.kanna.url).toBe("http://127.0.0.1:1234/mcp")
213
- })
214
-
215
- test("http user entry passes headers", () => {
216
- const cfg: McpServerConfig = {
217
- id: "x", name: "remote", enabled: true,
218
- createdAt: "", updatedAt: "", lastTest: { status: "untested" },
219
- transport: "http", url: "https://api.example.com/mcp", headers: { "x-key": "secret" },
220
- }
221
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [cfg]))
222
- expect(json.mcpServers.remote).toEqual({
223
- type: "http",
224
- url: "https://api.example.com/mcp",
225
- headers: { "x-key": "secret" },
226
- })
227
- })
228
-
229
- test("sse user entry uses type: sse", () => {
230
- const cfg: McpServerConfig = {
231
- id: "s", name: "events", enabled: true,
232
- createdAt: "", updatedAt: "", lastTest: { status: "untested" },
233
- transport: "sse", url: "https://example.com/sse", headers: {},
234
- }
235
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [cfg]))
236
- expect(json.mcpServers.events.type).toBe("sse")
237
- })
238
-
239
- test("ws user entry uses type: ws", () => {
240
- const cfg: McpServerConfig = {
241
- id: "w", name: "wsx", enabled: true,
242
- createdAt: "", updatedAt: "", lastTest: { status: "untested" },
243
- transport: "ws", url: "wss://example.com/ws", headers: {},
244
- }
245
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [cfg]))
246
- expect(json.mcpServers.wsx.type).toBe("ws")
247
- })
248
-
249
- test("multiple servers preserved in order", () => {
250
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [stdio("a"), stdio("b"), stdio("c")]))
251
- expect(Object.keys(json.mcpServers)).toEqual(["kanna", "a", "b", "c"])
252
- })
253
-
254
- test("injects oauth bearer header for authenticated server", () => {
255
- const cfg: McpServerConfig = {
256
- id: "s1", name: "design", enabled: true,
257
- createdAt: "", updatedAt: "", lastTest: { status: "untested" },
258
- transport: "http", url: "https://api.example/mcp", headers: {},
259
- oauth: { enabled: true, status: "authenticated", tokens: { access_token: "AT", token_type: "Bearer" } as never },
260
- }
261
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [cfg], new Map([["s1", "AT"]])))
262
- expect(json.mcpServers.design.headers.Authorization).toBe("Bearer AT")
263
- })
264
-
265
- test("omits Authorization when no bearer resolved", () => {
266
- const cfg: McpServerConfig = {
267
- id: "s2", name: "plain", enabled: true,
268
- createdAt: "", updatedAt: "", lastTest: { status: "untested" },
269
- transport: "http", url: "https://api.example/mcp", headers: {},
270
- }
271
- const json = JSON.parse(buildMcpConfigJson(HANDLE, [cfg], new Map()))
272
- expect(json.mcpServers.plain.headers?.Authorization).toBeUndefined()
273
- })
274
- })