@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,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
|
-
})
|