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.
@@ -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
+ }
@@ -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
+ })
@@ -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
+ }
@@ -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"