@cuongtran001/kanna 1.3.0 → 1.4.0
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/dist/client/assets/{arc-CAysfmX5.js → arc-BHj370W7.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
- package/dist/client/assets/channel-BP9IMEvd.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
- package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/chat-op-log.test.ts +63 -0
- package/src/server/chat-op-log.ts +71 -0
- package/src/server/chat-ops-diff.test.ts +120 -0
- package/src/server/chat-ops-diff.ts +97 -0
- package/src/server/chat-ops-parity.test.ts +140 -0
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +532 -0
- package/src/server/event-store-messages.adapter.ts +455 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +291 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +75 -0
- package/src/server/event-store.ts +257 -2285
- package/src/server/events.ts +1 -1
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/storage/backend.ts +7 -0
- package/src/server/storage/fs-storage.adapter.ts +21 -1
- package/src/server/storage/in-memory-storage.ts +12 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +560 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +360 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +238 -0
- package/src/server/ws-router.test.ts +90 -0
- package/src/server/ws-router.ts +340 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/chat-ops.test.ts +138 -0
- package/src/shared/chat-ops.ts +71 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/protocol.ts +3 -2
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +31 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test"
|
|
2
|
+
import { handleSettingsCommand, isSubagentValidationError } from "./ws-router-settings"
|
|
3
|
+
import type { SettingsCommandDeps } from "./ws-router-settings"
|
|
4
|
+
import type { AppSettingsSnapshot, Subagent, SubagentValidationError } from "../shared/types"
|
|
5
|
+
import type { ClientCommand } from "../shared/protocol"
|
|
6
|
+
import {
|
|
7
|
+
AUTH_DEFAULTS,
|
|
8
|
+
CLAUDE_AUTH_DEFAULTS,
|
|
9
|
+
CLAUDE_DRIVER_DEFAULTS,
|
|
10
|
+
CLAUDE_PTY_LIFECYCLE_DEFAULTS,
|
|
11
|
+
CLOUDFLARE_TUNNEL_DEFAULTS,
|
|
12
|
+
DEFAULT_OPENROUTER_SDK_MODEL,
|
|
13
|
+
UPLOAD_DEFAULTS,
|
|
14
|
+
} from "../shared/types"
|
|
15
|
+
import { KeybindingsManager } from "./keybindings"
|
|
16
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
17
|
+
import { tmpdir } from "node:os"
|
|
18
|
+
import path from "node:path"
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function makeSnapshot(): AppSettingsSnapshot {
|
|
25
|
+
return {
|
|
26
|
+
analyticsEnabled: true,
|
|
27
|
+
browserSettingsMigrated: false,
|
|
28
|
+
theme: "system",
|
|
29
|
+
chatSoundPreference: "always",
|
|
30
|
+
chatSoundId: "funk",
|
|
31
|
+
terminal: { scrollbackLines: 1_000, minColumnWidth: 450 },
|
|
32
|
+
editor: { preset: "cursor", commandTemplate: "cursor {path}" },
|
|
33
|
+
defaultProvider: "last_used",
|
|
34
|
+
providerDefaults: {
|
|
35
|
+
claude: { model: "claude-opus-4-7", modelOptions: { reasoningEffort: "high", contextWindow: "200k" }, planMode: false },
|
|
36
|
+
codex: { model: "gpt-5.5", modelOptions: { reasoningEffort: "high", fastMode: false }, planMode: false },
|
|
37
|
+
openrouter: { model: DEFAULT_OPENROUTER_SDK_MODEL, modelOptions: {}, planMode: false },
|
|
38
|
+
},
|
|
39
|
+
warning: null,
|
|
40
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
41
|
+
cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
|
|
42
|
+
auth: AUTH_DEFAULTS,
|
|
43
|
+
claudeAuth: CLAUDE_AUTH_DEFAULTS,
|
|
44
|
+
uploads: UPLOAD_DEFAULTS,
|
|
45
|
+
subagents: [],
|
|
46
|
+
customMcpServers: [],
|
|
47
|
+
customModels: [],
|
|
48
|
+
textSnippets: [],
|
|
49
|
+
claudeDriver: { ...CLAUDE_DRIVER_DEFAULTS, lifecycle: { ...CLAUDE_PTY_LIFECYCLE_DEFAULTS } },
|
|
50
|
+
globalPromptAppend: "",
|
|
51
|
+
shareDefaultTtlHours: 24,
|
|
52
|
+
subagentRuntime: { runTimeoutMs: 600_000, defaultLoopSubagentId: null },
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function makeDeps(tmpDir: string): Promise<SettingsCommandDeps & { sent: unknown[] }> {
|
|
57
|
+
const kb = new KeybindingsManager(path.join(tmpDir, "keybindings.json"))
|
|
58
|
+
const snapshot = makeSnapshot()
|
|
59
|
+
const sent: unknown[] = []
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
keybindings: kb,
|
|
63
|
+
resolvedAppSettings: {
|
|
64
|
+
getSnapshot: () => snapshot,
|
|
65
|
+
write: async (v) => ({ ...snapshot, analyticsEnabled: v.analyticsEnabled }),
|
|
66
|
+
writePatch: async () => snapshot,
|
|
67
|
+
setCloudflareTunnel: async () => snapshot,
|
|
68
|
+
setClaudeAuth: async () => snapshot,
|
|
69
|
+
createSubagent: async () => ({ id: "s1", name: "Test" }) as unknown as Subagent,
|
|
70
|
+
updateSubagent: async () => ({ id: "s1", name: "Test" }) as unknown as Subagent,
|
|
71
|
+
deleteSubagent: async () => {},
|
|
72
|
+
},
|
|
73
|
+
resolvedAnalytics: { track: mock(() => {}) },
|
|
74
|
+
resolvedLlmProvider: {
|
|
75
|
+
read: async () => ({ provider: "openai" as const, apiKey: "", model: "gpt-5.4-mini", baseUrl: "", resolvedBaseUrl: "https://api.openai.com/v1", enabled: false, warning: null, filePathDisplay: "~/.kanna/llm-provider.json" }),
|
|
76
|
+
write: async (v) => ({ ...v, resolvedBaseUrl: "https://api.openai.com/v1", enabled: false, warning: null, filePathDisplay: "~/.kanna/llm-provider.json" }),
|
|
77
|
+
validate: async () => ({ ok: false, error: { type: "config_error" as const, message: "stub" } }),
|
|
78
|
+
},
|
|
79
|
+
listOpenRouterModels: undefined,
|
|
80
|
+
send: (envelope) => { sent.push(envelope) },
|
|
81
|
+
sent,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// isSubagentValidationError
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
describe("isSubagentValidationError", () => {
|
|
90
|
+
test("returns true for validation error shape", () => {
|
|
91
|
+
const err: SubagentValidationError = { code: "EMPTY_NAME", message: "name is empty" }
|
|
92
|
+
expect(isSubagentValidationError(err)).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("returns false for Subagent shape", () => {
|
|
96
|
+
const subagent = { id: "s1", name: "Test", instructions: "" } as unknown as Subagent
|
|
97
|
+
expect(isSubagentValidationError(subagent)).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// handleSettingsCommand — selected cases
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
describe("handleSettingsCommand", () => {
|
|
106
|
+
test("settings.readKeybindings → acks with keybindings snapshot", async () => {
|
|
107
|
+
const tmpD = await mkdtemp(path.join(tmpdir(), "ws-settings-test-"))
|
|
108
|
+
try {
|
|
109
|
+
const deps = await makeDeps(tmpD)
|
|
110
|
+
const handled = await handleSettingsCommand(deps, { type: "settings.readKeybindings" }, "req-1")
|
|
111
|
+
expect(handled).toBe(true)
|
|
112
|
+
expect(deps.sent).toHaveLength(1)
|
|
113
|
+
const ack = deps.sent[0] as { type: string; id: string }
|
|
114
|
+
expect(ack.type).toBe("ack")
|
|
115
|
+
expect(ack.id).toBe("req-1")
|
|
116
|
+
} finally {
|
|
117
|
+
await rm(tmpD, { recursive: true, force: true })
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("settings.readAppSettings → acks with app-settings snapshot", async () => {
|
|
122
|
+
const tmpD = await mkdtemp(path.join(tmpdir(), "ws-settings-test-"))
|
|
123
|
+
try {
|
|
124
|
+
const deps = await makeDeps(tmpD)
|
|
125
|
+
const handled = await handleSettingsCommand(deps, { type: "settings.readAppSettings" }, "req-2")
|
|
126
|
+
expect(handled).toBe(true)
|
|
127
|
+
expect(deps.sent).toHaveLength(1)
|
|
128
|
+
const ack = deps.sent[0] as { type: string; result: AppSettingsSnapshot }
|
|
129
|
+
expect(ack.result.analyticsEnabled).toBe(true)
|
|
130
|
+
} finally {
|
|
131
|
+
await rm(tmpD, { recursive: true, force: true })
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("subagent.delete → acks ok", async () => {
|
|
136
|
+
const tmpD = await mkdtemp(path.join(tmpdir(), "ws-settings-test-"))
|
|
137
|
+
try {
|
|
138
|
+
const deps = await makeDeps(tmpD)
|
|
139
|
+
const handled = await handleSettingsCommand(deps, { type: "subagent.delete", id: "s1" }, "req-3")
|
|
140
|
+
expect(handled).toBe(true)
|
|
141
|
+
const ack = deps.sent[0] as { result: { ok: boolean } }
|
|
142
|
+
expect(ack.result.ok).toBe(true)
|
|
143
|
+
} finally {
|
|
144
|
+
await rm(tmpD, { recursive: true, force: true })
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("skills.listInstalled → acks with installed-skills snapshot", async () => {
|
|
149
|
+
const tmpD = await mkdtemp(path.join(tmpdir(), "ws-settings-test-"))
|
|
150
|
+
try {
|
|
151
|
+
const deps = await makeDeps(tmpD)
|
|
152
|
+
const handled = await handleSettingsCommand(deps, { type: "skills.listInstalled" }, "req-4")
|
|
153
|
+
expect(handled).toBe(true)
|
|
154
|
+
const ack = deps.sent[0] as { result: { skills: unknown[] } }
|
|
155
|
+
expect(Array.isArray(ack.result.skills)).toBe(true)
|
|
156
|
+
} finally {
|
|
157
|
+
await rm(tmpD, { recursive: true, force: true })
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test("unrecognized command type → returns false", async () => {
|
|
162
|
+
const tmpD = await mkdtemp(path.join(tmpdir(), "ws-settings-test-"))
|
|
163
|
+
try {
|
|
164
|
+
const deps = await makeDeps(tmpD)
|
|
165
|
+
// Cast via unknown to bypass TS narrowing — simulates an unknown command type
|
|
166
|
+
const handled = await handleSettingsCommand(deps, { type: "chat.create" } as unknown as ClientCommand, "req-5")
|
|
167
|
+
expect(handled).toBe(false)
|
|
168
|
+
expect(deps.sent).toHaveLength(0)
|
|
169
|
+
} finally {
|
|
170
|
+
await rm(tmpD, { recursive: true, force: true })
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-router-settings.ts
|
|
3
|
+
*
|
|
4
|
+
* Command handlers for the settings, subagent, MCP, LLM-provider, and
|
|
5
|
+
* skills command groups extracted from ws-router.ts. No closure
|
|
6
|
+
* dependencies on createWsRouter's local variables — every dep is injected
|
|
7
|
+
* via SettingsCommandDeps, making these handlers testable in isolation.
|
|
8
|
+
*
|
|
9
|
+
* Covers: settings.*, appSettings.*, subagent.*, settings.testMcpServer,
|
|
10
|
+
* settings.startMcpOAuth, settings.completeMcpOAuth, settings.*LlmProvider,
|
|
11
|
+
* settings.getChangelog, skills.*
|
|
12
|
+
*/
|
|
13
|
+
import { PROTOCOL_VERSION } from "../shared/types"
|
|
14
|
+
import type {
|
|
15
|
+
AppSettingsPatch,
|
|
16
|
+
AppSettingsSnapshot,
|
|
17
|
+
LlmProviderSnapshot,
|
|
18
|
+
LlmProviderValidationResult,
|
|
19
|
+
McpServerConfig,
|
|
20
|
+
OpenRouterModel,
|
|
21
|
+
Subagent,
|
|
22
|
+
SubagentInput,
|
|
23
|
+
SubagentPatch,
|
|
24
|
+
SubagentValidationError,
|
|
25
|
+
} from "../shared/types"
|
|
26
|
+
import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
|
|
27
|
+
import type { AnalyticsReporter } from "./analytics"
|
|
28
|
+
import { KeybindingsManager } from "./keybindings"
|
|
29
|
+
import { validateMcpServer } from "./mcp-validator"
|
|
30
|
+
import { startMcpOAuth, completeMcpOAuth, ensureFreshMcpToken } from "./mcp-oauth.adapter"
|
|
31
|
+
import { fetchGitHubReleases } from "./diff-store"
|
|
32
|
+
import { log } from "../shared/log"
|
|
33
|
+
import {
|
|
34
|
+
searchSkills,
|
|
35
|
+
installSkill,
|
|
36
|
+
uninstallSkill,
|
|
37
|
+
listInstalledSkills,
|
|
38
|
+
} from "./ws-router-skills"
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Dep interfaces (duck-typed; avoids circular imports with ws-router.ts)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface ResolvedAppSettings {
|
|
45
|
+
getSnapshot(): AppSettingsSnapshot
|
|
46
|
+
write(value: { analyticsEnabled: boolean }): Promise<AppSettingsSnapshot>
|
|
47
|
+
writePatch(patch: AppSettingsPatch): Promise<AppSettingsSnapshot>
|
|
48
|
+
setCloudflareTunnel(patch: Partial<AppSettingsSnapshot["cloudflareTunnel"]>): Promise<AppSettingsSnapshot>
|
|
49
|
+
setClaudeAuth(patch: Partial<AppSettingsSnapshot["claudeAuth"]>): Promise<AppSettingsSnapshot>
|
|
50
|
+
createSubagent(input: SubagentInput): Promise<Subagent | SubagentValidationError>
|
|
51
|
+
updateSubagent(id: string, patch: SubagentPatch): Promise<Subagent | SubagentValidationError>
|
|
52
|
+
deleteSubagent(id: string): Promise<void>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ResolvedLlmProvider {
|
|
56
|
+
read(): Promise<LlmProviderSnapshot>
|
|
57
|
+
write(value: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">): Promise<LlmProviderSnapshot>
|
|
58
|
+
validate(value: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">): Promise<LlmProviderValidationResult>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SettingsCommandDeps {
|
|
62
|
+
keybindings: KeybindingsManager
|
|
63
|
+
resolvedAppSettings: ResolvedAppSettings
|
|
64
|
+
resolvedAnalytics: Pick<AnalyticsReporter, "track">
|
|
65
|
+
resolvedLlmProvider: ResolvedLlmProvider
|
|
66
|
+
listOpenRouterModels: (() => Promise<OpenRouterModel[]>) | undefined
|
|
67
|
+
/** Pre-bound to the current WebSocket; called to ack the command. */
|
|
68
|
+
send: (envelope: ServerEnvelope) => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Internal helpers (previously at the bottom of ws-router.ts)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export function isSubagentValidationError(
|
|
76
|
+
value: Subagent | SubagentValidationError,
|
|
77
|
+
): value is SubagentValidationError {
|
|
78
|
+
return "code" in value && "message" in value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function testOAuthToken(token: string): Promise<{ ok: boolean; error: string | null }> {
|
|
82
|
+
const trimmed = typeof token === "string" ? token.trim() : ""
|
|
83
|
+
if (!trimmed) return { ok: false, error: "Token is empty" }
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"anthropic-version": "2023-06-01",
|
|
89
|
+
"content-type": "application/json",
|
|
90
|
+
"authorization": `Bearer ${trimmed}`,
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
model: "claude-haiku-4-5-20251001",
|
|
94
|
+
max_tokens: 1,
|
|
95
|
+
messages: [{ role: "user", content: "ok" }],
|
|
96
|
+
}),
|
|
97
|
+
signal: AbortSignal.timeout(10_000),
|
|
98
|
+
})
|
|
99
|
+
if (res.status === 401 || res.status === 403) return { ok: false, error: "Unauthorized" }
|
|
100
|
+
if (res.status === 429) return { ok: true, error: "Token valid but currently rate-limited" }
|
|
101
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }
|
|
102
|
+
return { ok: true, error: null }
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
105
|
+
return { ok: false, error: "Request timed out after 10s" }
|
|
106
|
+
}
|
|
107
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* For an OAuth-authenticated network server, resolve a fresh access token to
|
|
113
|
+
* inject as a Bearer when probing it — the manual "Test" / auto-test path is
|
|
114
|
+
* otherwise tokenless and a healthy OAuth server 401s. Returns undefined for
|
|
115
|
+
* stdio, static-header, or not-yet-authenticated servers (the probe runs with
|
|
116
|
+
* stored headers only). A refresh failure also yields undefined, so the probe
|
|
117
|
+
* surfaces the unauthorized error that correctly signals re-auth is needed.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveMcpTestBearer(
|
|
120
|
+
entry: McpServerConfig,
|
|
121
|
+
appSettings: { writePatch(p: AppSettingsPatch): Promise<unknown> },
|
|
122
|
+
): Promise<string | undefined> {
|
|
123
|
+
if (entry.transport === "stdio" || entry.oauth?.status !== "authenticated") return undefined
|
|
124
|
+
try {
|
|
125
|
+
return await ensureFreshMcpToken(entry, {
|
|
126
|
+
persist: (oauth) =>
|
|
127
|
+
void appSettings.writePatch({ customMcpServers: { setOAuthState: { id: entry.id, oauth } } }),
|
|
128
|
+
})
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function runMcpAutoTest(
|
|
135
|
+
id: string,
|
|
136
|
+
appSettings: { getSnapshot(): AppSettingsSnapshot; writePatch(p: AppSettingsPatch): Promise<unknown> },
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
const entry = appSettings.getSnapshot().customMcpServers.find((s) => s.id === id)
|
|
140
|
+
if (!entry) return
|
|
141
|
+
await appSettings.writePatch({
|
|
142
|
+
customMcpServers: {
|
|
143
|
+
setTestResult: { id, result: { status: "pending", startedAt: new Date().toISOString() } },
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
const bearer = await resolveMcpTestBearer(entry, appSettings)
|
|
147
|
+
const result = await validateMcpServer(entry, bearer ? { bearer } : {})
|
|
148
|
+
await appSettings.writePatch({ customMcpServers: { setTestResult: { id, result } } })
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// Auto-test must never throw; log + swallow.
|
|
151
|
+
log.warn("[kanna/ws-router] runMcpAutoTest failed", String(err))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Command dispatcher
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle one settings-domain WS command.
|
|
161
|
+
*
|
|
162
|
+
* Returns `true` when the command was handled (caller should `return`).
|
|
163
|
+
* Returns `false` when the command type falls outside this module's scope
|
|
164
|
+
* (should not happen in practice once all cases are wired up, but keeps the
|
|
165
|
+
* type-system honest).
|
|
166
|
+
*/
|
|
167
|
+
export async function handleSettingsCommand(
|
|
168
|
+
deps: SettingsCommandDeps,
|
|
169
|
+
command: ClientCommand,
|
|
170
|
+
id: string,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
const { keybindings, resolvedAppSettings, resolvedAnalytics, resolvedLlmProvider, listOpenRouterModels, send } = deps
|
|
173
|
+
|
|
174
|
+
switch (command.type) {
|
|
175
|
+
case "settings.readKeybindings": {
|
|
176
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
case "settings.writeKeybindings": {
|
|
180
|
+
const snapshot = await keybindings.write(command.bindings)
|
|
181
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
case "settings.readAppSettings": {
|
|
185
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: resolvedAppSettings.getSnapshot() })
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
case "settings.writeAppSettings": {
|
|
189
|
+
const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
|
|
190
|
+
if (previousAnalyticsEnabled && !command.analyticsEnabled) {
|
|
191
|
+
resolvedAnalytics.track("analytics_disabled")
|
|
192
|
+
}
|
|
193
|
+
const snapshot = await resolvedAppSettings.write({ analyticsEnabled: command.analyticsEnabled })
|
|
194
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
195
|
+
if (!previousAnalyticsEnabled && command.analyticsEnabled) {
|
|
196
|
+
resolvedAnalytics.track("analytics_enabled")
|
|
197
|
+
}
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
case "appSettings.setCloudflareTunnel": {
|
|
201
|
+
await resolvedAppSettings.setCloudflareTunnel(command.patch)
|
|
202
|
+
const snapshot = resolvedAppSettings.getSnapshot()
|
|
203
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
case "appSettings.setClaudeAuth": {
|
|
207
|
+
await resolvedAppSettings.setClaudeAuth(command.patch)
|
|
208
|
+
const snapshot = resolvedAppSettings.getSnapshot()
|
|
209
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
case "appSettings.testOAuthToken": {
|
|
213
|
+
const result = await testOAuthToken(command.token)
|
|
214
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
case "settings.writeAppSettingsPatch": {
|
|
218
|
+
const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
|
|
219
|
+
const snapshot = await resolvedAppSettings.writePatch(command.patch)
|
|
220
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
221
|
+
|
|
222
|
+
// Fire-and-forget auto-test for newly created or updated MCP server.
|
|
223
|
+
const targetId = (() => {
|
|
224
|
+
const ops = command.patch.customMcpServers
|
|
225
|
+
if (!ops) return null
|
|
226
|
+
if (ops.update) return ops.update.id
|
|
227
|
+
if (ops.create) {
|
|
228
|
+
// The created entry is the one with no prior match by name —
|
|
229
|
+
// simplest: pick the entry with the latest createdAt.
|
|
230
|
+
const list = snapshot.customMcpServers
|
|
231
|
+
if (list.length === 0) return null
|
|
232
|
+
return list.reduce((latest, e) => (e.createdAt > latest.createdAt ? e : latest), list[0]!).id
|
|
233
|
+
}
|
|
234
|
+
return null
|
|
235
|
+
})()
|
|
236
|
+
if (targetId) {
|
|
237
|
+
void runMcpAutoTest(targetId, resolvedAppSettings)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (command.patch.analyticsEnabled !== undefined && previousAnalyticsEnabled && !snapshot.analyticsEnabled) {
|
|
241
|
+
resolvedAnalytics.track("analytics_disabled")
|
|
242
|
+
}
|
|
243
|
+
if (command.patch.analyticsEnabled !== undefined && !previousAnalyticsEnabled && snapshot.analyticsEnabled) {
|
|
244
|
+
resolvedAnalytics.track("analytics_enabled")
|
|
245
|
+
}
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
case "subagent.create": {
|
|
249
|
+
const result = await resolvedAppSettings.createSubagent(command.input)
|
|
250
|
+
send({
|
|
251
|
+
v: PROTOCOL_VERSION,
|
|
252
|
+
type: "ack",
|
|
253
|
+
id,
|
|
254
|
+
result: isSubagentValidationError(result)
|
|
255
|
+
? { ok: false, error: result }
|
|
256
|
+
: { ok: true, subagent: result },
|
|
257
|
+
})
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
case "subagent.update": {
|
|
261
|
+
const result = await resolvedAppSettings.updateSubagent(command.id, command.patch)
|
|
262
|
+
send({
|
|
263
|
+
v: PROTOCOL_VERSION,
|
|
264
|
+
type: "ack",
|
|
265
|
+
id,
|
|
266
|
+
result: isSubagentValidationError(result)
|
|
267
|
+
? { ok: false, error: result }
|
|
268
|
+
: { ok: true, subagent: result },
|
|
269
|
+
})
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
case "subagent.delete": {
|
|
273
|
+
await resolvedAppSettings.deleteSubagent(command.id)
|
|
274
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true } })
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
case "settings.testMcpServer": {
|
|
278
|
+
const snapshot = resolvedAppSettings.getSnapshot()
|
|
279
|
+
const entry = snapshot.customMcpServers.find((s) => s.id === command.id)
|
|
280
|
+
if (!entry) {
|
|
281
|
+
send({
|
|
282
|
+
v: PROTOCOL_VERSION,
|
|
283
|
+
type: "ack",
|
|
284
|
+
id,
|
|
285
|
+
result: {
|
|
286
|
+
ok: false,
|
|
287
|
+
message: "MCP server not found",
|
|
288
|
+
lastTest: { status: "error", testedAt: new Date().toISOString(), message: "not found" } as const,
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
// Mark pending so the UI sees a spinner while we connect.
|
|
294
|
+
await resolvedAppSettings.writePatch({
|
|
295
|
+
customMcpServers: {
|
|
296
|
+
setTestResult: { id: entry.id, result: { status: "pending", startedAt: new Date().toISOString() } },
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
const testBearer = await resolveMcpTestBearer(entry, resolvedAppSettings)
|
|
300
|
+
const lastTest = await validateMcpServer(entry, testBearer ? { bearer: testBearer } : {})
|
|
301
|
+
await resolvedAppSettings.writePatch({
|
|
302
|
+
customMcpServers: { setTestResult: { id: entry.id, result: lastTest } },
|
|
303
|
+
})
|
|
304
|
+
send({
|
|
305
|
+
v: PROTOCOL_VERSION,
|
|
306
|
+
type: "ack",
|
|
307
|
+
id,
|
|
308
|
+
result: {
|
|
309
|
+
ok: lastTest.status === "ok",
|
|
310
|
+
message: lastTest.status === "error" ? lastTest.message : undefined,
|
|
311
|
+
lastTest,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
case "settings.startMcpOAuth": {
|
|
317
|
+
const snapshot = resolvedAppSettings.getSnapshot()
|
|
318
|
+
const entry = snapshot.customMcpServers.find((s) => s.id === command.id)
|
|
319
|
+
if (!entry || entry.transport === "stdio") {
|
|
320
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: "not found or unsupported transport" } })
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const result = await startMcpOAuth(entry, {
|
|
325
|
+
persist: (oauth) => void resolvedAppSettings.writePatch({ customMcpServers: { setOAuthState: { id: entry.id, oauth } } }),
|
|
326
|
+
})
|
|
327
|
+
send({
|
|
328
|
+
v: PROTOCOL_VERSION, type: "ack", id,
|
|
329
|
+
result: result.kind === "authorizationUrl"
|
|
330
|
+
? { ok: true, authorizationUrl: result.authorizationUrl }
|
|
331
|
+
: { ok: true, alreadyAuthenticated: true },
|
|
332
|
+
})
|
|
333
|
+
} catch (err) {
|
|
334
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: err instanceof Error ? err.message : "oauth start failed" } })
|
|
335
|
+
}
|
|
336
|
+
return true
|
|
337
|
+
}
|
|
338
|
+
case "settings.completeMcpOAuth": {
|
|
339
|
+
const snapshot = resolvedAppSettings.getSnapshot()
|
|
340
|
+
const entry = snapshot.customMcpServers.find((s) => s.id === command.id)
|
|
341
|
+
if (!entry || entry.transport === "stdio") {
|
|
342
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: "not found" } })
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const result = await completeMcpOAuth(entry, command.callbackUrl, {
|
|
347
|
+
persist: (oauth) => void resolvedAppSettings.writePatch({ customMcpServers: { setOAuthState: { id: entry.id, oauth } } }),
|
|
348
|
+
listTools: async (_serverUrl, accessToken) => {
|
|
349
|
+
const r = await validateMcpServer(entry, { bearer: accessToken })
|
|
350
|
+
return r.status === "ok" ? r.toolCount : 0
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true, testResult: result } })
|
|
354
|
+
} catch (err) {
|
|
355
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: err instanceof Error ? err.message : "oauth complete failed" } })
|
|
356
|
+
}
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
359
|
+
case "settings.readLlmProvider": {
|
|
360
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: await resolvedLlmProvider.read() })
|
|
361
|
+
return true
|
|
362
|
+
}
|
|
363
|
+
case "settings.listOpenRouterModels": {
|
|
364
|
+
const models = listOpenRouterModels ? await listOpenRouterModels() : []
|
|
365
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: models })
|
|
366
|
+
return true
|
|
367
|
+
}
|
|
368
|
+
case "settings.getChangelog": {
|
|
369
|
+
const releases = await fetchGitHubReleases("cuongtranba/kanna")
|
|
370
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: releases })
|
|
371
|
+
return true
|
|
372
|
+
}
|
|
373
|
+
case "settings.writeLlmProvider": {
|
|
374
|
+
const snapshot = await resolvedLlmProvider.write({
|
|
375
|
+
provider: command.provider,
|
|
376
|
+
apiKey: command.apiKey,
|
|
377
|
+
model: command.model,
|
|
378
|
+
baseUrl: command.baseUrl,
|
|
379
|
+
})
|
|
380
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
381
|
+
return true
|
|
382
|
+
}
|
|
383
|
+
case "settings.validateLlmProvider": {
|
|
384
|
+
const result = await resolvedLlmProvider.validate({
|
|
385
|
+
provider: command.provider,
|
|
386
|
+
apiKey: command.apiKey,
|
|
387
|
+
model: command.model,
|
|
388
|
+
baseUrl: command.baseUrl,
|
|
389
|
+
})
|
|
390
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
391
|
+
return true
|
|
392
|
+
}
|
|
393
|
+
case "skills.search": {
|
|
394
|
+
const snapshot = await searchSkills(command.query, command.limit)
|
|
395
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
396
|
+
return true
|
|
397
|
+
}
|
|
398
|
+
case "skills.install": {
|
|
399
|
+
const result = await installSkill(command.source, command.skillId)
|
|
400
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
401
|
+
return true
|
|
402
|
+
}
|
|
403
|
+
case "skills.uninstall": {
|
|
404
|
+
const result = await uninstallSkill(command.skillId)
|
|
405
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
406
|
+
return true
|
|
407
|
+
}
|
|
408
|
+
case "skills.listInstalled": {
|
|
409
|
+
const result = await listInstalledSkills()
|
|
410
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
411
|
+
return true
|
|
412
|
+
}
|
|
413
|
+
default:
|
|
414
|
+
return false
|
|
415
|
+
}
|
|
416
|
+
}
|