kanna-code 0.16.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 +32 -0
- package/dist/client/assets/{index-DMxCijCf.js → index-CtKZIZdD.js} +113 -113
- package/dist/client/assets/index-Cwex1U8S.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +4 -1
- package/src/server/cli-runtime.test.ts +130 -0
- package/src/server/cli-runtime.ts +43 -3
- package/src/server/event-store.ts +7 -31
- package/src/server/provider-catalog.test.ts +1 -1
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/shared/dev-ports.test.ts +32 -0
- package/src/shared/dev-ports.ts +55 -0
- package/src/shared/types.ts +3 -3
- package/dist/client/assets/index-BET6YVYr.css +0 -32
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFile, mkdir,
|
|
1
|
+
import { appendFile, mkdir, rename, writeFile } from "node:fs/promises"
|
|
2
2
|
import { existsSync, readFileSync as readFileSyncImmediate } from "node:fs"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
@@ -588,8 +588,8 @@ export class EventStore {
|
|
|
588
588
|
return (await this.getLegacyTranscriptStats()).hasLegacyData
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
private createSnapshot(
|
|
592
|
-
|
|
591
|
+
private createSnapshot(): SnapshotFile {
|
|
592
|
+
return {
|
|
593
593
|
v: STORE_VERSION,
|
|
594
594
|
generatedAt: Date.now(),
|
|
595
595
|
projects: this.listProjects().map((project) => ({ ...project })),
|
|
@@ -597,30 +597,10 @@ export class EventStore {
|
|
|
597
597
|
.filter((chat) => !chat.deletedAt)
|
|
598
598
|
.map((chat) => ({ ...chat })),
|
|
599
599
|
}
|
|
600
|
-
|
|
601
|
-
if (includeLegacyMessages) {
|
|
602
|
-
snapshot.messages = [...this.legacyMessagesByChatId.entries()].map(([chatId, entries]) => ({
|
|
603
|
-
chatId,
|
|
604
|
-
entries: cloneTranscriptEntries(entries),
|
|
605
|
-
}))
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
return snapshot
|
|
609
600
|
}
|
|
610
601
|
|
|
611
602
|
async compact() {
|
|
612
|
-
const snapshot = this.createSnapshot(
|
|
613
|
-
await Bun.write(this.snapshotPath, JSON.stringify(snapshot, null, 2))
|
|
614
|
-
await Promise.all([
|
|
615
|
-
Bun.write(this.projectsLogPath, ""),
|
|
616
|
-
Bun.write(this.chatsLogPath, ""),
|
|
617
|
-
Bun.write(this.messagesLogPath, ""),
|
|
618
|
-
Bun.write(this.turnsLogPath, ""),
|
|
619
|
-
])
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
private async compactLegacyMessagesIntoSnapshot() {
|
|
623
|
-
const snapshot = this.createSnapshot(true)
|
|
603
|
+
const snapshot = this.createSnapshot()
|
|
624
604
|
await Bun.write(this.snapshotPath, JSON.stringify(snapshot, null, 2))
|
|
625
605
|
await Promise.all([
|
|
626
606
|
Bun.write(this.projectsLogPath, ""),
|
|
@@ -636,17 +616,14 @@ export class EventStore {
|
|
|
636
616
|
|
|
637
617
|
const sourceSummary = stats.sources.map((source) => source === "messages_log" ? "messages.jsonl" : "snapshot.json").join(", ")
|
|
638
618
|
onProgress?.(`${LOG_PREFIX} transcript migration detected: ${stats.chatCount} chats, ${stats.entryCount} entries from ${sourceSummary}`)
|
|
639
|
-
onProgress?.(`${LOG_PREFIX} transcript migration: compacting legacy transcript state into snapshot`)
|
|
640
|
-
await this.compactLegacyMessagesIntoSnapshot()
|
|
641
619
|
|
|
642
|
-
const
|
|
643
|
-
const messageSets = snapshot.messages ?? []
|
|
620
|
+
const messageSets = [...this.legacyMessagesByChatId.entries()]
|
|
644
621
|
onProgress?.(`${LOG_PREFIX} transcript migration: writing ${messageSets.length} per-chat transcript files`)
|
|
645
622
|
|
|
646
623
|
await mkdir(this.transcriptsDir, { recursive: true })
|
|
647
624
|
const logEveryChat = messageSets.length <= 10
|
|
648
625
|
for (let index = 0; index < messageSets.length; index += 1) {
|
|
649
|
-
const
|
|
626
|
+
const [chatId, entries] = messageSets[index]
|
|
650
627
|
const transcriptPath = this.transcriptPath(chatId)
|
|
651
628
|
const tempPath = `${transcriptPath}.tmp`
|
|
652
629
|
const payload = entries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
@@ -657,9 +634,8 @@ export class EventStore {
|
|
|
657
634
|
}
|
|
658
635
|
}
|
|
659
636
|
|
|
660
|
-
delete snapshot.messages
|
|
661
|
-
await Bun.write(this.snapshotPath, JSON.stringify(snapshot, null, 2))
|
|
662
637
|
this.clearLegacyTranscriptState()
|
|
638
|
+
await this.compact()
|
|
663
639
|
this.cachedTranscript = null
|
|
664
640
|
onProgress?.(`${LOG_PREFIX} transcript migration complete`)
|
|
665
641
|
return true
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { ensureCloudflaredInstalled, logShareDetails, startShareTunnel } from "./share"
|
|
3
|
+
|
|
4
|
+
describe("ensureCloudflaredInstalled", () => {
|
|
5
|
+
test("returns immediately when the binary already exists", async () => {
|
|
6
|
+
const installCalls: string[] = []
|
|
7
|
+
|
|
8
|
+
const result = await ensureCloudflaredInstalled({
|
|
9
|
+
cloudflaredBin: "/tmp/cloudflared",
|
|
10
|
+
existsSync: () => true,
|
|
11
|
+
installCloudflared: async (to) => {
|
|
12
|
+
installCalls.push(to)
|
|
13
|
+
return to
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
expect(result).toBe("/tmp/cloudflared")
|
|
18
|
+
expect(installCalls).toEqual([])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("installs the binary on demand when it is missing", async () => {
|
|
22
|
+
const installCalls: string[] = []
|
|
23
|
+
const logLines: string[] = []
|
|
24
|
+
|
|
25
|
+
const result = await ensureCloudflaredInstalled({
|
|
26
|
+
cloudflaredBin: "/tmp/cloudflared",
|
|
27
|
+
existsSync: () => false,
|
|
28
|
+
installCloudflared: async (to) => {
|
|
29
|
+
installCalls.push(to)
|
|
30
|
+
return to
|
|
31
|
+
},
|
|
32
|
+
log: (message) => {
|
|
33
|
+
logLines.push(message)
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(result).toBe("/tmp/cloudflared")
|
|
38
|
+
expect(installCalls).toEqual(["/tmp/cloudflared"])
|
|
39
|
+
expect(logLines).toEqual(["installing cloudflared binary"])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe("startShareTunnel", () => {
|
|
44
|
+
test("starts a quick tunnel after ensuring the binary exists", async () => {
|
|
45
|
+
const installCalls: string[] = []
|
|
46
|
+
const quickTunnelUrls: string[] = []
|
|
47
|
+
let stopCalls = 0
|
|
48
|
+
|
|
49
|
+
const shareTunnel = await startShareTunnel("http://localhost:3333", {
|
|
50
|
+
cloudflaredBin: "/tmp/cloudflared",
|
|
51
|
+
existsSync: () => false,
|
|
52
|
+
installCloudflared: async (to) => {
|
|
53
|
+
installCalls.push(to)
|
|
54
|
+
return to
|
|
55
|
+
},
|
|
56
|
+
createQuickTunnel: (localUrl) => {
|
|
57
|
+
quickTunnelUrls.push(localUrl)
|
|
58
|
+
return {
|
|
59
|
+
once(event, listener) {
|
|
60
|
+
if (event === "url") {
|
|
61
|
+
queueMicrotask(() => (listener as (url: string) => void)("https://kanna.trycloudflare.com"))
|
|
62
|
+
}
|
|
63
|
+
return this
|
|
64
|
+
},
|
|
65
|
+
off(_event, _listener) {
|
|
66
|
+
return this
|
|
67
|
+
},
|
|
68
|
+
stop() {
|
|
69
|
+
stopCalls += 1
|
|
70
|
+
return true
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(installCalls).toEqual(["/tmp/cloudflared"])
|
|
77
|
+
expect(quickTunnelUrls).toEqual(["http://localhost:3333"])
|
|
78
|
+
expect(shareTunnel.publicUrl).toBe("https://kanna.trycloudflare.com")
|
|
79
|
+
shareTunnel.stop()
|
|
80
|
+
expect(stopCalls).toBe(1)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe("logShareDetails", () => {
|
|
85
|
+
test("prints qr, public url, and local url in the expected order", async () => {
|
|
86
|
+
const logLines: string[] = []
|
|
87
|
+
|
|
88
|
+
await logShareDetails(
|
|
89
|
+
(message) => {
|
|
90
|
+
logLines.push(message)
|
|
91
|
+
},
|
|
92
|
+
"https://kanna.trycloudflare.com",
|
|
93
|
+
"http://localhost:3333",
|
|
94
|
+
async (url) => `[qr:${url}]\n`,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(logLines).toEqual([
|
|
98
|
+
"QR Code:",
|
|
99
|
+
"[qr:https://kanna.trycloudflare.com]",
|
|
100
|
+
"",
|
|
101
|
+
"Public URL:",
|
|
102
|
+
"https://kanna.trycloudflare.com",
|
|
103
|
+
"",
|
|
104
|
+
"Local URL:",
|
|
105
|
+
"http://localhost:3333",
|
|
106
|
+
])
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -42,7 +42,7 @@ export type ServiceTier = "fast"
|
|
|
42
42
|
|
|
43
43
|
export interface ClaudeModelOptions {
|
|
44
44
|
reasoningEffort: ClaudeReasoningEffort
|
|
45
|
-
contextWindow
|
|
45
|
+
contextWindow: ClaudeContextWindow
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface CodexModelOptions {
|
|
@@ -140,9 +140,9 @@ export function getClaudeContextWindowOptions(modelId: string): readonly Provide
|
|
|
140
140
|
return getClaudeModelOption(modelId)?.contextWindowOptions ?? []
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
export function normalizeClaudeContextWindow(modelId: string, contextWindow?: unknown): ClaudeContextWindow
|
|
143
|
+
export function normalizeClaudeContextWindow(modelId: string, contextWindow?: unknown): ClaudeContextWindow {
|
|
144
144
|
const options = getClaudeContextWindowOptions(modelId)
|
|
145
|
-
if (options.length === 0) return
|
|
145
|
+
if (options.length === 0) return DEFAULT_CLAUDE_MODEL_OPTIONS.contextWindow
|
|
146
146
|
return options.some((option) => option.id === contextWindow)
|
|
147
147
|
? contextWindow as ClaudeContextWindow
|
|
148
148
|
: DEFAULT_CLAUDE_MODEL_OPTIONS.contextWindow
|