kanna-code 0.15.0 → 0.17.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/README.md +42 -0
- package/dist/client/assets/index-CtKZIZdD.js +533 -0
- package/dist/client/assets/index-Cwex1U8S.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +4 -1
- package/src/server/agent.ts +4 -2
- package/src/server/cli-runtime.test.ts +130 -0
- package/src/server/cli-runtime.ts +48 -4
- package/src/server/codex-app-server-protocol.ts +8 -1
- package/src/server/codex-app-server.ts +1 -1
- package/src/server/event-store.test.ts +112 -1
- package/src/server/event-store.ts +149 -29
- package/src/server/events.ts +1 -3
- package/src/server/provider-catalog.test.ts +29 -1
- package/src/server/provider-catalog.ts +8 -1
- package/src/server/read-models.test.ts +1 -1
- package/src/server/read-models.ts +3 -3
- package/src/server/server.ts +2 -0
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/server/ws-router.ts +1 -1
- package/src/shared/dev-ports.test.ts +32 -0
- package/src/shared/dev-ports.ts +55 -0
- package/src/shared/types.ts +40 -2
- package/dist/client/assets/index-C6R1P_HL.js +0 -523
- package/dist/client/assets/index-CA-kJR8F.css +0 -32
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import QRCode from "qrcode"
|
|
3
|
+
import { Tunnel, bin as cloudflaredBin, install as installCloudflared } from "cloudflared"
|
|
4
|
+
|
|
5
|
+
export interface StartedShareTunnel {
|
|
6
|
+
publicUrl: string
|
|
7
|
+
stop: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ShareTunnelProcess {
|
|
11
|
+
once(event: "url", listener: (url: string) => void): ShareTunnelProcess
|
|
12
|
+
once(event: "error", listener: (error: Error) => void): ShareTunnelProcess
|
|
13
|
+
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): ShareTunnelProcess
|
|
14
|
+
off(event: "url", listener: (url: string) => void): ShareTunnelProcess
|
|
15
|
+
off(event: "error", listener: (error: Error) => void): ShareTunnelProcess
|
|
16
|
+
off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): ShareTunnelProcess
|
|
17
|
+
stop(): boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ShareTunnelDeps {
|
|
21
|
+
cloudflaredBin?: string
|
|
22
|
+
existsSync?: (path: string) => boolean
|
|
23
|
+
installCloudflared?: (to: string) => Promise<string>
|
|
24
|
+
createQuickTunnel?: (localUrl: string) => ShareTunnelProcess
|
|
25
|
+
log?: (message: string) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function renderTerminalQr(url: string) {
|
|
29
|
+
return QRCode.toString(url, {
|
|
30
|
+
type: "terminal",
|
|
31
|
+
small: true,
|
|
32
|
+
errorCorrectionLevel: "M",
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function ensureCloudflaredInstalled(
|
|
37
|
+
deps: ShareTunnelDeps = {},
|
|
38
|
+
) {
|
|
39
|
+
const resolvedBin = deps.cloudflaredBin ?? cloudflaredBin
|
|
40
|
+
const fileExists = deps.existsSync ?? existsSync
|
|
41
|
+
const installBinary = deps.installCloudflared ?? installCloudflared
|
|
42
|
+
|
|
43
|
+
if (fileExists(resolvedBin)) {
|
|
44
|
+
return resolvedBin
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
deps.log?.("installing cloudflared binary")
|
|
48
|
+
await installBinary(resolvedBin)
|
|
49
|
+
return resolvedBin
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function startShareTunnel(localUrl: string, deps: ShareTunnelDeps = {}): Promise<StartedShareTunnel> {
|
|
53
|
+
await ensureCloudflaredInstalled(deps)
|
|
54
|
+
const tunnel = (deps.createQuickTunnel ?? ((url) => Tunnel.quick(url)))(localUrl)
|
|
55
|
+
|
|
56
|
+
const publicUrl = await new Promise<string>((resolve, reject) => {
|
|
57
|
+
let settled = false
|
|
58
|
+
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
tunnel.off("url", handleUrl)
|
|
61
|
+
tunnel.off("error", handleError)
|
|
62
|
+
tunnel.off("exit", handleExit)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const settle = (callback: () => void) => {
|
|
66
|
+
if (settled) return
|
|
67
|
+
settled = true
|
|
68
|
+
cleanup()
|
|
69
|
+
callback()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleUrl = (url: string) => {
|
|
73
|
+
settle(() => resolve(url))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handleError = (error: Error) => {
|
|
77
|
+
settle(() => reject(error))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
81
|
+
settle(() => reject(new Error(`Cloudflare tunnel exited before a public URL was ready (code: ${String(code)}, signal: ${String(signal)})`)))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
tunnel.once("url", handleUrl)
|
|
85
|
+
tunnel.once("error", handleError)
|
|
86
|
+
tunnel.once("exit", handleExit)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
publicUrl,
|
|
91
|
+
stop: () => {
|
|
92
|
+
tunnel.stop()
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function logShareDetails(
|
|
98
|
+
log: (message: string) => void,
|
|
99
|
+
publicUrl: string,
|
|
100
|
+
localUrl: string,
|
|
101
|
+
renderShareQrImpl: (url: string) => Promise<string> = renderTerminalQr,
|
|
102
|
+
) {
|
|
103
|
+
const qrCode = await renderShareQrImpl(publicUrl)
|
|
104
|
+
|
|
105
|
+
log("QR Code:")
|
|
106
|
+
log(qrCode.trimEnd())
|
|
107
|
+
log("")
|
|
108
|
+
log("Public URL:")
|
|
109
|
+
log(publicUrl)
|
|
110
|
+
log("")
|
|
111
|
+
log("Local URL:")
|
|
112
|
+
log(localUrl)
|
|
113
|
+
}
|
package/src/server/ws-router.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function createWsRouter({
|
|
|
121
121
|
id,
|
|
122
122
|
snapshot: {
|
|
123
123
|
type: "chat",
|
|
124
|
-
data: deriveChatSnapshot(store.state, agent.getActiveStatuses(), topic.chatId),
|
|
124
|
+
data: deriveChatSnapshot(store.state, agent.getActiveStatuses(), topic.chatId, (chatId) => store.getMessages(chatId)),
|
|
125
125
|
},
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_DEV_CLIENT_PORT,
|
|
4
4
|
getDefaultDevServerPort,
|
|
5
|
+
parseDevArgs,
|
|
5
6
|
resolveDevPorts,
|
|
6
7
|
stripPortArg,
|
|
7
8
|
} from "./dev-ports"
|
|
@@ -50,3 +51,34 @@ describe("stripPortArg", () => {
|
|
|
50
51
|
])
|
|
51
52
|
})
|
|
52
53
|
})
|
|
54
|
+
|
|
55
|
+
describe("parseDevArgs", () => {
|
|
56
|
+
test("derives dev share options from the client port", () => {
|
|
57
|
+
expect(parseDevArgs(["--share", "--port", "3333"], "dev-machine")).toEqual({
|
|
58
|
+
clientPort: 3333,
|
|
59
|
+
serverPort: 3334,
|
|
60
|
+
share: true,
|
|
61
|
+
backendTargetHost: "127.0.0.1",
|
|
62
|
+
allowedHosts: true,
|
|
63
|
+
serverArgs: [],
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("rejects combining --share with --host or --remote", () => {
|
|
68
|
+
expect(() => parseDevArgs(["--share", "--host", "dev-box"], "dev-machine")).toThrow("--share cannot be used with --host")
|
|
69
|
+
expect(() => parseDevArgs(["--host", "dev-box", "--share"], "dev-machine")).toThrow("--share cannot be used with --host")
|
|
70
|
+
expect(() => parseDevArgs(["--share", "--remote"], "dev-machine")).toThrow("--share cannot be used with --remote")
|
|
71
|
+
expect(() => parseDevArgs(["--remote", "--share"], "dev-machine")).toThrow("--share cannot be used with --remote")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("preserves existing remote host behavior when share is off", () => {
|
|
75
|
+
expect(parseDevArgs(["--remote", "--port", "3333"], "dev-machine")).toEqual({
|
|
76
|
+
clientPort: 3333,
|
|
77
|
+
serverPort: 3334,
|
|
78
|
+
share: false,
|
|
79
|
+
backendTargetHost: "127.0.0.1",
|
|
80
|
+
allowedHosts: true,
|
|
81
|
+
serverArgs: ["--remote"],
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
package/src/shared/dev-ports.ts
CHANGED
|
@@ -4,6 +4,15 @@ export function getDefaultDevServerPort(clientPort = DEFAULT_DEV_CLIENT_PORT) {
|
|
|
4
4
|
return clientPort + 1
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export interface DevArgResolution {
|
|
8
|
+
clientPort: number
|
|
9
|
+
serverPort: number
|
|
10
|
+
share: boolean
|
|
11
|
+
backendTargetHost: string
|
|
12
|
+
allowedHosts: true | string[]
|
|
13
|
+
serverArgs: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export function resolveDevPorts(args: string[]) {
|
|
8
17
|
let clientPort = DEFAULT_DEV_CLIENT_PORT
|
|
9
18
|
|
|
@@ -41,3 +50,49 @@ export function stripPortArg(args: string[]) {
|
|
|
41
50
|
|
|
42
51
|
return stripped
|
|
43
52
|
}
|
|
53
|
+
|
|
54
|
+
export function parseDevArgs(args: string[], localHostname: string): DevArgResolution {
|
|
55
|
+
const { clientPort, serverPort } = resolveDevPorts(args)
|
|
56
|
+
const serverArgs = stripPortArg(args).filter((arg) => arg !== "--share")
|
|
57
|
+
let share = false
|
|
58
|
+
let sawHost = false
|
|
59
|
+
let sawRemote = false
|
|
60
|
+
let backendTargetHost = "127.0.0.1"
|
|
61
|
+
let allowAllHosts = false
|
|
62
|
+
const hosts = new Set<string>(["localhost", "127.0.0.1", "0.0.0.0", localHostname])
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
65
|
+
const arg = args[index]
|
|
66
|
+
if (arg === "--share") {
|
|
67
|
+
if (sawHost) throw new Error("--share cannot be used with --host")
|
|
68
|
+
if (sawRemote) throw new Error("--share cannot be used with --remote")
|
|
69
|
+
share = true
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--remote") {
|
|
73
|
+
if (share) throw new Error("--share cannot be used with --remote")
|
|
74
|
+
sawRemote = true
|
|
75
|
+
backendTargetHost = "127.0.0.1"
|
|
76
|
+
allowAllHosts = true
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (arg !== "--host") continue
|
|
80
|
+
|
|
81
|
+
const next = args[index + 1]
|
|
82
|
+
if (!next || next.startsWith("-")) continue
|
|
83
|
+
if (share) throw new Error("--share cannot be used with --host")
|
|
84
|
+
sawHost = true
|
|
85
|
+
hosts.add(next)
|
|
86
|
+
backendTargetHost = next === "0.0.0.0" ? "127.0.0.1" : next
|
|
87
|
+
index += 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
clientPort,
|
|
92
|
+
serverPort,
|
|
93
|
+
share,
|
|
94
|
+
backendTargetHost,
|
|
95
|
+
allowedHosts: share || allowAllHosts ? true : [...hosts],
|
|
96
|
+
serverArgs,
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface ProviderModelOption {
|
|
|
7
7
|
id: string
|
|
8
8
|
label: string
|
|
9
9
|
supportsEffort: boolean
|
|
10
|
+
contextWindowOptions?: readonly ProviderContextWindowOption[]
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface ProviderEffortOption {
|
|
@@ -14,6 +15,11 @@ export interface ProviderEffortOption {
|
|
|
14
15
|
label: string
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export interface ProviderContextWindowOption {
|
|
19
|
+
id: ClaudeContextWindow
|
|
20
|
+
label: string
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export const CLAUDE_REASONING_OPTIONS = [
|
|
18
24
|
{ id: "low", label: "Low" },
|
|
19
25
|
{ id: "medium", label: "Medium" },
|
|
@@ -31,10 +37,12 @@ export const CODEX_REASONING_OPTIONS = [
|
|
|
31
37
|
|
|
32
38
|
export type ClaudeReasoningEffort = (typeof CLAUDE_REASONING_OPTIONS)[number]["id"]
|
|
33
39
|
export type CodexReasoningEffort = (typeof CODEX_REASONING_OPTIONS)[number]["id"]
|
|
40
|
+
export type ClaudeContextWindow = "200k" | "1m"
|
|
34
41
|
export type ServiceTier = "fast"
|
|
35
42
|
|
|
36
43
|
export interface ClaudeModelOptions {
|
|
37
44
|
reasoningEffort: ClaudeReasoningEffort
|
|
45
|
+
contextWindow: ClaudeContextWindow
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export interface CodexModelOptions {
|
|
@@ -53,6 +61,7 @@ export type ModelOptions = Partial<{
|
|
|
53
61
|
|
|
54
62
|
export const DEFAULT_CLAUDE_MODEL_OPTIONS = {
|
|
55
63
|
reasoningEffort: "high",
|
|
64
|
+
contextWindow: "200k",
|
|
56
65
|
} as const satisfies ClaudeModelOptions
|
|
57
66
|
|
|
58
67
|
export const DEFAULT_CODEX_MODEL_OPTIONS = {
|
|
@@ -68,6 +77,15 @@ export function isCodexReasoningEffort(value: unknown): value is CodexReasoningE
|
|
|
68
77
|
return CODEX_REASONING_OPTIONS.some((option) => option.id === value)
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
export const CLAUDE_CONTEXT_WINDOW_OPTIONS = [
|
|
81
|
+
{ id: "200k", label: "200k" },
|
|
82
|
+
{ id: "1m", label: "1M" },
|
|
83
|
+
] as const satisfies readonly ProviderContextWindowOption[]
|
|
84
|
+
|
|
85
|
+
export function isClaudeContextWindow(value: unknown): value is ClaudeContextWindow {
|
|
86
|
+
return CLAUDE_CONTEXT_WINDOW_OPTIONS.some((option) => option.id === value)
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
export interface ProviderCatalogEntry {
|
|
72
90
|
id: AgentProvider
|
|
73
91
|
label: string
|
|
@@ -86,8 +104,8 @@ export const PROVIDERS: ProviderCatalogEntry[] = [
|
|
|
86
104
|
defaultEffort: "high",
|
|
87
105
|
supportsPlanMode: true,
|
|
88
106
|
models: [
|
|
89
|
-
{ id: "opus", label: "Opus", supportsEffort: true },
|
|
90
|
-
{ id: "sonnet", label: "Sonnet", supportsEffort: true },
|
|
107
|
+
{ id: "opus", label: "Opus", supportsEffort: true, contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS] },
|
|
108
|
+
{ id: "sonnet", label: "Sonnet", supportsEffort: true, contextWindowOptions: [...CLAUDE_CONTEXT_WINDOW_OPTIONS] },
|
|
91
109
|
{ id: "haiku", label: "Haiku", supportsEffort: true },
|
|
92
110
|
],
|
|
93
111
|
efforts: [...CLAUDE_REASONING_OPTIONS],
|
|
@@ -114,6 +132,26 @@ export function getProviderCatalog(provider: AgentProvider): ProviderCatalogEntr
|
|
|
114
132
|
return entry
|
|
115
133
|
}
|
|
116
134
|
|
|
135
|
+
export function getClaudeModelOption(modelId: string): ProviderModelOption | undefined {
|
|
136
|
+
return getProviderCatalog("claude").models.find((candidate) => candidate.id === modelId)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getClaudeContextWindowOptions(modelId: string): readonly ProviderContextWindowOption[] {
|
|
140
|
+
return getClaudeModelOption(modelId)?.contextWindowOptions ?? []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function normalizeClaudeContextWindow(modelId: string, contextWindow?: unknown): ClaudeContextWindow {
|
|
144
|
+
const options = getClaudeContextWindowOptions(modelId)
|
|
145
|
+
if (options.length === 0) return DEFAULT_CLAUDE_MODEL_OPTIONS.contextWindow
|
|
146
|
+
return options.some((option) => option.id === contextWindow)
|
|
147
|
+
? contextWindow as ClaudeContextWindow
|
|
148
|
+
: DEFAULT_CLAUDE_MODEL_OPTIONS.contextWindow
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function resolveClaudeApiModelId(modelId: string, contextWindow?: ClaudeContextWindow): string {
|
|
152
|
+
return contextWindow === "1m" ? `${modelId}[1m]` : modelId
|
|
153
|
+
}
|
|
154
|
+
|
|
117
155
|
export type KannaStatus =
|
|
118
156
|
| "idle"
|
|
119
157
|
| "starting"
|