@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,194 +0,0 @@
1
- import { registerSdkToolOnMcpServer } from "./mcp-zod-compat.adapter"
2
- import { randomBytes, randomUUID } from "node:crypto"
3
- import { closeHttpServer, createHttpServer, listen, type HttpIncomingMessage } from "./http-server.adapter"
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
5
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
6
- import type { ServerNotification } from "@modelcontextprotocol/sdk/types.js"
7
- import { KANNA_MCP_SERVER_NAME } from "../shared/tools"
8
- import { buildKannaMcpTools, type KannaMcpArgs } from "./kanna-mcp"
9
- import type { McpServerConfig } from "../shared/types"
10
- import type { JsonObject } from "../shared/json"
11
-
12
- function isServerNotification(
13
- value: ChannelNotification,
14
- ): value is ChannelNotification & ServerNotification {
15
- return typeof value.method === "string"
16
- }
17
-
18
- export interface ChannelNotification {
19
- method: "notifications/claude/channel"
20
- params: { content: string; meta: JsonObject; _meta?: JsonObject }
21
- }
22
-
23
- export function buildChannelNotification(
24
- content: string,
25
- meta: JsonObject = {},
26
- ): ChannelNotification {
27
- return {
28
- method: "notifications/claude/channel",
29
- params: { content, meta: { source: KANNA_MCP_SERVER_NAME, ...meta } },
30
- }
31
- }
32
-
33
- export interface KannaMcpHttpHandle {
34
- url: string
35
- bearerToken: string
36
- close: () => Promise<void>
37
- channelClientReady: Promise<void>
38
- pushChannelPrompt: (content: string, meta?: JsonObject) => Promise<void>
39
- }
40
-
41
- export interface StartKannaMcpHttpServerOptions {
42
- args: KannaMcpArgs
43
- host?: string
44
- port?: number
45
- }
46
-
47
- export async function startKannaMcpHttpServer(
48
- opts: StartKannaMcpHttpServerOptions,
49
- ): Promise<KannaMcpHttpHandle> {
50
- const bearerToken = randomBytes(32).toString("hex")
51
- const host = opts.host ?? "127.0.0.1"
52
- const port = opts.port ?? 0
53
-
54
- const mcp = new McpServer(
55
- { name: KANNA_MCP_SERVER_NAME, version: "1.0.0" },
56
- {
57
- capabilities: {
58
- experimental: {
59
- "claude/channel": {},
60
- "claude/channel/permission": {},
61
- },
62
- },
63
- },
64
- )
65
-
66
- const tools = buildKannaMcpTools(opts.args)
67
- for (const def of tools) {
68
- registerSdkToolOnMcpServer(mcp, def)
69
- }
70
-
71
- const transport = new StreamableHTTPServerTransport({
72
- sessionIdGenerator: () => randomUUID(),
73
- })
74
- await mcp.connect(transport)
75
-
76
- let resolveReady: () => void = () => {}
77
- const channelClientReady = new Promise<void>((resolve) => {
78
- resolveReady = resolve
79
- })
80
- mcp.server.oninitialized = () => {
81
- resolveReady()
82
- }
83
-
84
- const pushChannelPrompt = async (
85
- content: string,
86
- meta: JsonObject = {},
87
- ): Promise<void> => {
88
- const notification = buildChannelNotification(content, meta)
89
- try {
90
- if (isServerNotification(notification)) {
91
- await mcp.server.notification(notification)
92
- }
93
- } catch (err) {
94
- if (mcp.isConnected()) throw err
95
- }
96
- }
97
-
98
- const httpServer = createHttpServer((req, res) => {
99
- if (!authorize(req, bearerToken)) {
100
- res.statusCode = 401
101
- res.setHeader("WWW-Authenticate", "Bearer")
102
- res.end("unauthorized")
103
- return
104
- }
105
- void transport.handleRequest(req, res).catch((err) => {
106
- if (!res.headersSent) {
107
- res.statusCode = 500
108
- res.end(String(err))
109
- }
110
- })
111
- })
112
-
113
- let address
114
- try {
115
- address = await listen(httpServer, port, host)
116
- } catch (err) {
117
- try { await transport.close() } catch { }
118
- throw err
119
- }
120
-
121
- const url = `http://${host}:${address.port}/mcp`
122
-
123
- let closed = false
124
- const close = async (): Promise<void> => {
125
- if (closed) return
126
- closed = true
127
- try {
128
- await transport.close()
129
- } catch {
130
- }
131
- await closeHttpServer(httpServer)
132
- }
133
-
134
- return { url, bearerToken, close, channelClientReady, pushChannelPrompt }
135
- }
136
-
137
- function authorize(req: HttpIncomingMessage, bearerToken: string): boolean {
138
- const header = req.headers.authorization
139
- if (!header || typeof header !== "string") return false
140
- const prefix = "Bearer "
141
- if (!header.startsWith(prefix)) return false
142
- const supplied = header.slice(prefix.length).trim()
143
- return constantTimeEqual(supplied, bearerToken)
144
- }
145
-
146
- function constantTimeEqual(a: string, b: string): boolean {
147
- if (a.length !== b.length) return false
148
- let mismatch = 0
149
- for (let i = 0; i < a.length; i++) {
150
- mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i)
151
- }
152
- return mismatch === 0
153
- }
154
-
155
-
156
- export function buildMcpConfigJson(
157
- handle: { url: string; bearerToken: string },
158
- userServers: readonly McpServerConfig[] = [],
159
- oauthBearers: ReadonlyMap<string, string> = new Map(),
160
- ): string {
161
- const mcpServers: Record<string, JsonObject> = {
162
- [KANNA_MCP_SERVER_NAME]: {
163
- type: "http",
164
- url: handle.url,
165
- headers: {
166
- Authorization: `Bearer ${handle.bearerToken}`,
167
- },
168
- },
169
- }
170
- for (const s of userServers) {
171
- if (!s.enabled) continue
172
- if (s.name === KANNA_MCP_SERVER_NAME) continue
173
- mcpServers[s.name] = toClaudeCliMcpEntry(s, oauthBearers.get(s.id))
174
- }
175
- return JSON.stringify({ mcpServers })
176
- }
177
-
178
- function toClaudeCliMcpEntry(s: McpServerConfig, oauthBearer?: string): JsonObject {
179
- if (s.transport === "stdio") {
180
- return {
181
- type: "stdio",
182
- command: s.command,
183
- args: s.args,
184
- env: s.env,
185
- ...(s.cwd ? { cwd: s.cwd } : {}),
186
- }
187
- }
188
- const headers = oauthBearer ? { ...s.headers, Authorization: `Bearer ${oauthBearer}` } : s.headers
189
- return {
190
- type: s.transport,
191
- url: s.url,
192
- headers,
193
- }
194
- }
@@ -1,108 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtemp, rm } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { POLICY_DEFAULT } from "../../shared/permission-policy"
6
- import { createToolCallbackService } from "../tool-callback"
7
- import { createTestEventStore } from "../storage/test-helpers"
8
- import { createAskUserQuestionTool } from "./ask-user-question"
9
-
10
- async function newStore() {
11
- const dir = await mkdtemp(path.join(tmpdir(), "kanna-mcp-aq-"))
12
- const store = createTestEventStore(dir)
13
- await store.initialize()
14
- const cleanup = async () => {
15
- await new Promise<void>((r) => setTimeout(r, 50))
16
- await rm(dir, { recursive: true, force: true })
17
- }
18
- return { store, dir, cleanup }
19
- }
20
-
21
- const handlerCtx = () => ({
22
- chatId: "c1",
23
- sessionId: "s1",
24
- toolUseId: "tu1",
25
- cwd: "/tmp",
26
- chatPolicy: POLICY_DEFAULT,
27
- })
28
-
29
- describe("mcp__kanna__ask_user_question", () => {
30
- test("calls policy.evaluate then routes to tool-callback", async () => {
31
- const { store, cleanup } = await newStore()
32
- try {
33
- const svc = createToolCallbackService({
34
- store, serverSecret: "k", now: () => 1,
35
- })
36
- const tool = createAskUserQuestionTool({ toolCallback: svc })
37
- const inputArgs = {
38
- questions: [{
39
- text: "ok?",
40
- header: "OK",
41
- options: [{ label: "yes", description: "" }, { label: "no", description: "" }],
42
- multiSelect: false,
43
- }],
44
- }
45
- const promise = tool.handler(inputArgs, handlerCtx())
46
- const pending = await store.listPendingToolRequests("c1")
47
- expect(pending).toHaveLength(1)
48
- await svc.answer(pending[0].id, { kind: "answer", payload: { answers: { "ok?": "yes" } } })
49
- const result = await promise
50
- expect(result.content[0].type).toBe("text")
51
- expect(JSON.parse(result.content[0].text).answers).toEqual({ "ok?": "yes" })
52
- expect(result.isError).toBeFalsy()
53
- } finally { await cleanup() }
54
- })
55
-
56
- test("auto-deny chatPolicy still routes through ask (UI), denial only via cancel/cancelAllForChat", async () => {
57
- const { store, cleanup } = await newStore()
58
- try {
59
- const svc = createToolCallbackService({
60
- store, serverSecret: "k", now: () => 1,
61
- })
62
- const tool = createAskUserQuestionTool({ toolCallback: svc })
63
- const promise = tool.handler(
64
- {
65
- questions: [{
66
- text: "x",
67
- header: "X",
68
- options: [{ label: "a", description: "" }, { label: "b", description: "" }],
69
- multiSelect: false,
70
- }],
71
- },
72
- { ...handlerCtx(), chatPolicy: { ...POLICY_DEFAULT, defaultAction: "auto-deny" } },
73
- )
74
- const pending = await store.listPendingToolRequests("c1")
75
- expect(pending).toHaveLength(1)
76
- await svc.cancelAllForChat("c1", "user-cancel")
77
- const result = await promise
78
- expect(result.isError).toBe(true)
79
- expect(result.content[0].type).toBe("text")
80
- expect(typeof result.content[0].text).toBe("string")
81
- } finally { await cleanup() }
82
- })
83
-
84
- test("answer decision with no payload → throws loudly (no silent {} coercion)", async () => {
85
- const { store, cleanup } = await newStore()
86
- try {
87
- const svc = createToolCallbackService({
88
- store, serverSecret: "k", now: () => 1,
89
- })
90
- const tool = createAskUserQuestionTool({ toolCallback: svc })
91
- const promise = tool.handler(
92
- {
93
- questions: [{
94
- text: "ok?",
95
- header: "OK",
96
- options: [{ label: "yes", description: "" }, { label: "no", description: "" }],
97
- multiSelect: false,
98
- }],
99
- },
100
- handlerCtx(),
101
- )
102
- const pending = await store.listPendingToolRequests("c1")
103
- expect(pending).toHaveLength(1)
104
- await svc.answer(pending[0].id, { kind: "allow" as const })
105
- await expect(promise).rejects.toThrow(/empty answer payload/i)
106
- } finally { await cleanup() }
107
- })
108
- })
@@ -1,55 +0,0 @@
1
- import { z } from "zod"
2
- import type { ToolCallbackService } from "../tool-callback"
3
- import type { ToolHandlerContext, ToolHandlerResult } from "./tool-callback-shim"
4
- import { gatedToolCall } from "./tool-callback-shim"
5
-
6
- const QuestionSchema = z.object({
7
- text: z.string(),
8
- header: z.string(),
9
- options: z.array(z.object({ label: z.string(), description: z.string() })).min(2).max(4),
10
- multiSelect: z.boolean(),
11
- })
12
-
13
- const InputSchema = z.object({
14
- questions: z.array(QuestionSchema).min(1).max(4),
15
- })
16
-
17
- export type AskUserQuestionInput = z.infer<typeof InputSchema>
18
-
19
- export interface AskUserQuestionTool {
20
- name: "ask_user_question"
21
- schema: typeof InputSchema
22
- handler: (input: AskUserQuestionInput, ctx: ToolHandlerContext) => Promise<ToolHandlerResult>
23
- }
24
-
25
- export function createAskUserQuestionTool(deps: { toolCallback: ToolCallbackService }): AskUserQuestionTool {
26
- return {
27
- name: "ask_user_question",
28
- schema: InputSchema,
29
- async handler(input, ctx) {
30
- return gatedToolCall({
31
- toolCallback: deps.toolCallback,
32
- toolName: "mcp__kanna__ask_user_question",
33
- ctx,
34
- args: input,
35
- formatAnswer: (payload) => {
36
- if (payload === undefined || payload === null || typeof payload !== "object") {
37
- throw new Error(
38
- "mcp__kanna__ask_user_question: empty answer payload "
39
- + `(received ${payload === undefined ? "undefined" : typeof payload}). `
40
- + "This means the policy gate or tool-callback resolved the request without a user response — "
41
- + "interactive tools must always go through the ask/UI path. See issue #215.",
42
- )
43
- }
44
- return {
45
- content: [{ type: "text" as const, text: JSON.stringify(payload) }],
46
- }
47
- },
48
- formatDeny: (reason) => ({
49
- content: [{ type: "text" as const, text: `Denied: ${reason}` }],
50
- isError: true,
51
- }),
52
- })
53
- },
54
- }
55
- }
@@ -1,61 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtemp, rm } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { POLICY_DEFAULT } from "../../shared/permission-policy"
6
- import { createToolCallbackService } from "../tool-callback"
7
- import { createTestEventStore } from "../storage/test-helpers"
8
- import { createExitPlanModeTool } from "./exit-plan-mode"
9
-
10
- async function newStore() {
11
- const dir = await mkdtemp(path.join(tmpdir(), "kanna-mcp-epm-"))
12
- const store = createTestEventStore(dir)
13
- await store.initialize()
14
- const cleanup = async () => {
15
- await new Promise<void>((r) => setTimeout(r, 50))
16
- await rm(dir, { recursive: true, force: true })
17
- }
18
- return { store, dir, cleanup }
19
- }
20
-
21
- const handlerCtx = () => ({
22
- chatId: "c",
23
- sessionId: "s",
24
- toolUseId: "tu",
25
- cwd: "/tmp",
26
- chatPolicy: POLICY_DEFAULT,
27
- })
28
-
29
- describe("mcp__kanna__exit_plan_mode", () => {
30
- test("confirmed answer → returns success content", async () => {
31
- const { store, cleanup } = await newStore()
32
- try {
33
- const svc = createToolCallbackService({
34
- store, serverSecret: "k", now: () => 1,
35
- })
36
- const tool = createExitPlanModeTool({ toolCallback: svc })
37
- const promise = tool.handler({ plan: "do x" }, handlerCtx())
38
- const pending = await store.listPendingToolRequests("c")
39
- await svc.answer(pending[0].id, { kind: "answer", payload: { confirmed: true } })
40
- const result = await promise
41
- expect(result.isError).toBeFalsy()
42
- expect(result.content[0].text).toContain("confirmed")
43
- } finally { await cleanup() }
44
- })
45
-
46
- test("rejected with message → isError true with message", async () => {
47
- const { store, cleanup } = await newStore()
48
- try {
49
- const svc = createToolCallbackService({
50
- store, serverSecret: "k", now: () => 1,
51
- })
52
- const tool = createExitPlanModeTool({ toolCallback: svc })
53
- const promise = tool.handler({ plan: "do x" }, handlerCtx())
54
- const pending = await store.listPendingToolRequests("c")
55
- await svc.answer(pending[0].id, { kind: "answer", payload: { confirmed: false, message: "tweak step 3" } })
56
- const result = await promise
57
- expect(result.isError).toBe(true)
58
- expect(result.content[0].text).toContain("tweak step 3")
59
- } finally { await cleanup() }
60
- })
61
- })
@@ -1,49 +0,0 @@
1
- import { z } from "zod"
2
- import type { ToolCallbackService } from "../tool-callback"
3
- import type { ToolHandlerContext, ToolHandlerResult } from "./tool-callback-shim"
4
- import { gatedToolCall } from "./tool-callback-shim"
5
- import { isRecord } from "../../shared/errors"
6
-
7
- const InputSchema = z.object({
8
- plan: z.string(),
9
- })
10
-
11
- export type ExitPlanModeInput = z.infer<typeof InputSchema>
12
-
13
- export interface ExitPlanModeTool {
14
- name: "exit_plan_mode"
15
- schema: typeof InputSchema
16
- handler: (input: ExitPlanModeInput, ctx: ToolHandlerContext) => Promise<ToolHandlerResult>
17
- }
18
-
19
- export function createExitPlanModeTool(deps: { toolCallback: ToolCallbackService }): ExitPlanModeTool {
20
- return {
21
- name: "exit_plan_mode",
22
- schema: InputSchema,
23
- async handler(input, ctx) {
24
- return gatedToolCall({
25
- toolCallback: deps.toolCallback,
26
- toolName: "mcp__kanna__exit_plan_mode",
27
- ctx,
28
- args: input,
29
- formatAnswer: (payload) => {
30
- const record = isRecord(payload) ? payload : {}
31
- if (record.confirmed) {
32
- return {
33
- content: [{ type: "text" as const, text: JSON.stringify({ confirmed: true }) }],
34
- }
35
- }
36
- const msg = typeof record.message === "string" ? record.message : "User wants to suggest edits."
37
- return {
38
- content: [{ type: "text" as const, text: msg }],
39
- isError: true,
40
- }
41
- },
42
- formatDeny: (reason) => ({
43
- content: [{ type: "text" as const, text: `Denied: ${reason}` }],
44
- isError: true,
45
- }),
46
- })
47
- },
48
- }
49
- }
@@ -1,20 +0,0 @@
1
- import type { SdkMcpToolDefinition } from "@anthropic-ai/claude-agent-sdk"
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
3
- import type { ShapeOutput, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js"
4
- import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"
5
- import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js"
6
-
7
- export function registerSdkToolOnMcpServer(mcp: McpServer, def: SdkMcpToolDefinition): void {
8
- const handler = async (
9
- input: ShapeOutput<ZodRawShapeCompat>,
10
- extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
11
- ) => await def.handler(<never>input, <never>extra)
12
- mcp.registerTool(
13
- def.name,
14
- {
15
- description: def.description,
16
- inputSchema: <ZodRawShapeCompat><unknown>def.inputSchema,
17
- },
18
- handler,
19
- )
20
- }
@@ -1,43 +0,0 @@
1
- export type PtyInstancePhase =
2
- | "spawning"
3
- | "trust-dialog"
4
- | "ready"
5
- | "streaming"
6
- | "cancelling"
7
- | "exited"
8
-
9
- export type PtyInstanceSmokeTest = "pending" | "pass" | "fail"
10
-
11
- export interface PtyInstanceState {
12
- chatId: string
13
- sessionId: string | null
14
- pid: number | null
15
- cwd: string
16
- model: string
17
- accountLabel: string | null
18
- oauthMasked: string | null
19
- phase: PtyInstancePhase
20
- startedAt: number
21
- lastEventAt: number
22
- turnCount: number
23
- tokensIn: number
24
- tokensOut: number
25
- planMode: boolean | null
26
- smokeTest: PtyInstanceSmokeTest | null
27
- outputRingTail: string | null
28
- exitedAt: number | null
29
- exitCode: number | null
30
- rssBytes: number | null
31
- rssPeakBytes: number | null
32
- cpuPercent: number | null
33
- cpuPeakPercent: number | null
34
- }
35
-
36
- export type PtyInstanceDelta =
37
- | { type: "added"; instance: PtyInstanceState }
38
- | { type: "updated"; instance: PtyInstanceState }
39
- | { type: "removed"; chatId: string }
40
-
41
- export interface PtyInstancesSnapshot {
42
- instances: PtyInstanceState[]
43
- }