@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.
- package/README.md +5 -9
- package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
- package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
- package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
- package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
- package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
- package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
- package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
- package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
- package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
- package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
- package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
- package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
- package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
- package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
- package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
- package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
- package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
- package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
- package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
- package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
- package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
- package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
- package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
- package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
- package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
- package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
- package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
- package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
- package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
- package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
- package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
- package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
- package/dist/client/assets/index-CxDE9VOy.css +1 -0
- package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
- package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
- package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
- package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
- package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
- package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
- package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
- package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
- package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
- package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
- package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
- package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
- package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
- package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
- package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
- package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
- package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
- package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
- package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
- package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
- package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
- package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
- package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
- package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
- package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
- package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
- package/dist/client/index.html +35 -35
- package/package.json +2 -2
- package/src/server/agent-coordinator-types.ts +0 -6
- package/src/server/agent-coordinator.ts +6 -45
- package/src/server/agent.compaction.test.ts +1 -13
- package/src/server/agent.oauth-account-info.test.ts +0 -3
- package/src/server/agent.oauth-release.test.ts +0 -2
- package/src/server/agent.oauth-rotation.test.ts +0 -9
- package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
- package/src/server/agent.openrouter-model.test.ts +0 -2
- package/src/server/agent.openrouter-watchdog.test.ts +0 -3
- package/src/server/agent.sdk-workflow-register.test.ts +2 -64
- package/src/server/agent.test.ts +1 -319
- package/src/server/app-settings.test.ts +14 -17
- package/src/server/app-settings.ts +22 -37
- package/src/server/auto-continue/e2e.test.ts +0 -1
- package/src/server/background-task-output-registry.test.ts +1 -1
- package/src/server/background-task-output-registry.ts +1 -1
- package/src/server/claude-cancel-handler.test.ts +0 -58
- package/src/server/claude-cancel-handler.ts +1 -17
- package/src/server/claude-harness-stream.test.ts +134 -0
- package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
- package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
- package/src/server/claude-send-command.test.ts +0 -1
- package/src/server/claude-session-config-helpers.test.ts +1 -90
- package/src/server/claude-session-config-helpers.ts +1 -38
- package/src/server/claude-session-error-handler.test.ts +0 -1
- package/src/server/claude-session-lifecycle.test.ts +2 -36
- package/src/server/claude-session-lifecycle.ts +2 -8
- package/src/server/claude-session-runner.test.ts +8 -36
- package/src/server/claude-session-runner.ts +1 -26
- package/src/server/claude-session-spawner.test.ts +5 -64
- package/src/server/claude-session-spawner.ts +49 -115
- package/src/server/claude-session-start.test.ts +1 -14
- package/src/server/claude-session-start.ts +0 -8
- package/src/server/claude-session-state-queries.test.ts +1 -2
- package/src/server/claude-session-state.test.ts +0 -1
- package/src/server/claude-session-state.ts +0 -4
- package/src/server/claude-subagent-wiring.test.ts +2 -68
- package/src/server/claude-subagent-wiring.ts +3 -50
- package/src/server/claude-turn-starter-types.ts +0 -3
- package/src/server/claude-turn-starter.test.ts +0 -28
- package/src/server/claude-turn-starter.ts +0 -5
- package/src/server/event-store.test.ts +0 -2
- package/src/server/events.ts +1 -2
- package/src/server/harness-types.ts +1 -2
- package/src/server/import-subagent-drill.e2e.test.ts +1 -1
- package/src/server/imported-subagents-dir.test.ts +1 -1
- package/src/server/imported-subagents-dir.ts +1 -1
- package/src/server/kanna-mcp.test.ts +0 -75
- package/src/server/kanna-mcp.ts +1 -47
- package/src/server/server.test.ts +5 -7
- package/src/server/server.ts +2 -18
- package/src/server/subagent-orchestrator.test.ts +0 -1
- package/src/server/subagent-provider-run.test.ts +48 -10
- package/src/server/subagent-provider-run.ts +12 -3
- package/src/server/tool-callback.test.ts +1 -1
- package/src/server/ws-router-agent-ctrl.test.ts +0 -45
- package/src/server/ws-router-agent-ctrl.ts +1 -31
- package/src/server/ws-router-broadcast.ts +0 -27
- package/src/server/ws-router-defaults.test.ts +2 -3
- package/src/server/ws-router-defaults.ts +2 -3
- package/src/server/ws-router-envelope.ts +0 -15
- package/src/server/ws-router-observability.test.ts +1 -1
- package/src/server/ws-router-settings.test.ts +2 -2
- package/src/server/ws-router.test.ts +2 -2
- package/src/server/ws-router.ts +0 -8
- package/src/shared/app-settings-types.ts +9 -20
- package/src/shared/claude-api-error-classification.ts +1 -1
- package/src/shared/protocol.ts +1 -11
- package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
- package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
- package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
- package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
- package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
- package/dist/client/assets/index-C-UYaDk6.css +0 -1
- package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
- package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
- package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
- package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
- package/src/server/agent.pty-rotation.test.ts +0 -322
- package/src/server/claude-pty/auth.test.ts +0 -38
- package/src/server/claude-pty/auth.ts +0 -17
- package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
- package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
- package/src/server/claude-pty/driver.test.ts +0 -1153
- package/src/server/claude-pty/driver.ts +0 -829
- package/src/server/claude-pty/env.test.ts +0 -79
- package/src/server/claude-pty/env.ts +0 -20
- package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
- package/src/server/claude-pty/jsonl-to-event.ts +0 -357
- package/src/server/claude-pty/parity-matrix.test.ts +0 -437
- package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
- package/src/server/claude-pty/pid-registry.test.ts +0 -178
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
- package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
- package/src/server/claude-pty/pty-instance-registry.ts +0 -172
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
- package/src/server/claude-pty/pty-process.adapter.ts +0 -60
- package/src/server/claude-pty/pty-process.test.ts +0 -49
- package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
- package/src/server/claude-pty/resolve-binary.test.ts +0 -118
- package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
- package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
- package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
- package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
- package/src/server/claude-pty/sandbox/platform.ts +0 -27
- package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
- package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
- package/src/server/claude-pty/sandbox/preflight.ts +0 -44
- package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
- package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
- package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
- package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
- package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
- package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
- package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
- package/src/server/claude-pty/settings-writer.test.ts +0 -22
- package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
- package/src/server/claude-pty/smoke-test.test.ts +0 -199
- package/src/server/claude-pty/smoke-test.ts +0 -182
- package/src/server/claude-pty/tui-control.test.ts +0 -300
- package/src/server/claude-pty/tui-control.ts +0 -174
- package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
- package/src/server/claude-pty/tui-source.adapter.ts +0 -294
- package/src/server/http-server.adapter.test.ts +0 -18
- package/src/server/http-server.adapter.ts +0 -39
- package/src/server/kanna-mcp-http.test.ts +0 -274
- package/src/server/kanna-mcp-http.ts +0 -194
- package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
- package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
- package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
- package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
- package/src/server/mcp-zod-compat.adapter.ts +0 -20
- package/src/shared/pty-instance.ts +0 -43
- /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
- /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
|
-
}
|
|
File without changes
|
|
File without changes
|