kanna-code 0.13.9 → 0.15.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 +18 -2
- package/dist/client/assets/{index-BypLQCc0.js → index-C6R1P_HL.js} +120 -120
- package/dist/client/index.html +1 -1
- package/package.json +3 -2
- package/src/server/cli-runtime.test.ts +55 -0
- package/src/server/cli-runtime.ts +25 -8
- package/src/server/quick-response.test.ts +29 -0
- package/src/server/quick-response.ts +40 -11
- package/src/server/server.ts +3 -0
- package/src/shared/dev-ports.test.ts +52 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +0 -1
- package/src/shared/types.ts +1 -1
package/dist/client/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Kanna</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-C6R1P_HL.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-CA-kJR8F.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kanna-code",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"description": "A beautiful web UI for Claude Code",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"check": "tsc --noEmit && vite build",
|
|
38
38
|
"dev": "bun run ./scripts/dev.ts",
|
|
39
39
|
"dev:client": "vite --host 0.0.0.0 --port 5174",
|
|
40
|
-
"dev:server": "bun run ./scripts/dev-server.ts --no-open --port
|
|
40
|
+
"dev:server": "bun run ./scripts/dev-server.ts --no-open --port 5175",
|
|
41
|
+
"install:dev": "bun install && bun run build && bun install -g .",
|
|
41
42
|
"start": "bun run ./src/server/cli.ts",
|
|
42
43
|
"test": "bun test",
|
|
43
44
|
"prepublishOnly": "vite build"
|
|
@@ -22,6 +22,7 @@ function createDeps(overrides: Partial<Parameters<typeof runCli>[1]> = {}) {
|
|
|
22
22
|
const calls = {
|
|
23
23
|
startServer: [] as Array<{
|
|
24
24
|
port: number
|
|
25
|
+
host: string
|
|
25
26
|
openBrowser: boolean
|
|
26
27
|
strictPort: boolean
|
|
27
28
|
update: {
|
|
@@ -81,6 +82,7 @@ describe("parseArgs", () => {
|
|
|
81
82
|
kind: "run",
|
|
82
83
|
options: {
|
|
83
84
|
port: 4000,
|
|
85
|
+
host: "127.0.0.1",
|
|
84
86
|
openBrowser: false,
|
|
85
87
|
strictPort: false,
|
|
86
88
|
},
|
|
@@ -92,12 +94,54 @@ describe("parseArgs", () => {
|
|
|
92
94
|
kind: "run",
|
|
93
95
|
options: {
|
|
94
96
|
port: 3210,
|
|
97
|
+
host: "127.0.0.1",
|
|
95
98
|
openBrowser: true,
|
|
96
99
|
strictPort: true,
|
|
97
100
|
},
|
|
98
101
|
})
|
|
99
102
|
})
|
|
100
103
|
|
|
104
|
+
test("--remote without value binds all interfaces", () => {
|
|
105
|
+
expect(parseArgs(["--remote"])).toEqual({
|
|
106
|
+
kind: "run",
|
|
107
|
+
options: {
|
|
108
|
+
port: 3210,
|
|
109
|
+
host: "0.0.0.0",
|
|
110
|
+
openBrowser: true,
|
|
111
|
+
strictPort: false,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("--host with IP binds to that address", () => {
|
|
117
|
+
expect(parseArgs(["--host", "100.64.0.1"])).toEqual({
|
|
118
|
+
kind: "run",
|
|
119
|
+
options: {
|
|
120
|
+
port: 3210,
|
|
121
|
+
host: "100.64.0.1",
|
|
122
|
+
openBrowser: true,
|
|
123
|
+
strictPort: false,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("--host with hostname binds to that name", () => {
|
|
129
|
+
expect(parseArgs(["--host", "dev-box"])).toEqual({
|
|
130
|
+
kind: "run",
|
|
131
|
+
options: {
|
|
132
|
+
port: 3210,
|
|
133
|
+
host: "dev-box",
|
|
134
|
+
openBrowser: true,
|
|
135
|
+
strictPort: false,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("--host without a value throws", () => {
|
|
141
|
+
expect(() => parseArgs(["--host"])).toThrow("Missing value for --host")
|
|
142
|
+
expect(() => parseArgs(["--host", "--no-open"])).toThrow("Missing value for --host")
|
|
143
|
+
})
|
|
144
|
+
|
|
101
145
|
test("returns version and help actions without running startup", () => {
|
|
102
146
|
expect(parseArgs(["--version"])).toEqual({ kind: "version" })
|
|
103
147
|
expect(parseArgs(["--help"])).toEqual({ kind: "help" })
|
|
@@ -146,6 +190,7 @@ describe("runCli", () => {
|
|
|
146
190
|
expect(calls.startServer).toHaveLength(1)
|
|
147
191
|
expect(calls.startServer[0]).toMatchObject({
|
|
148
192
|
port: 4000,
|
|
193
|
+
host: "127.0.0.1",
|
|
149
194
|
openBrowser: false,
|
|
150
195
|
strictPort: false,
|
|
151
196
|
update: {
|
|
@@ -180,6 +225,7 @@ describe("runCli", () => {
|
|
|
180
225
|
})
|
|
181
226
|
|
|
182
227
|
test("opens the root route in the browser", async () => {
|
|
228
|
+
delete process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]
|
|
183
229
|
const { calls, deps } = createDeps()
|
|
184
230
|
|
|
185
231
|
await runCli(["--port", "4000"], deps)
|
|
@@ -187,6 +233,15 @@ describe("runCli", () => {
|
|
|
187
233
|
expect(calls.openUrl).toEqual(["http://localhost:4000"])
|
|
188
234
|
})
|
|
189
235
|
|
|
236
|
+
test("opens browser at hostname when --host <host> is given", async () => {
|
|
237
|
+
delete process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR]
|
|
238
|
+
const { calls, deps } = createDeps()
|
|
239
|
+
|
|
240
|
+
await runCli(["--host", "dev-box", "--port", "4000"], deps)
|
|
241
|
+
|
|
242
|
+
expect(calls.openUrl).toEqual(["http://dev-box:4000"])
|
|
243
|
+
})
|
|
244
|
+
|
|
190
245
|
test("suppresses browser open for a ui-triggered restarted child", async () => {
|
|
191
246
|
process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] = "1"
|
|
192
247
|
const { calls, deps } = createDeps()
|
|
@@ -8,6 +8,7 @@ import { CLI_SUPPRESS_OPEN_ONCE_ENV_VAR } from "./restart"
|
|
|
8
8
|
|
|
9
9
|
export interface CliOptions {
|
|
10
10
|
port: number
|
|
11
|
+
host: string
|
|
11
12
|
openBrowser: boolean
|
|
12
13
|
strictPort: boolean
|
|
13
14
|
}
|
|
@@ -69,15 +70,18 @@ Usage:
|
|
|
69
70
|
${CLI_COMMAND} [options]
|
|
70
71
|
|
|
71
72
|
Options:
|
|
72
|
-
--port <number>
|
|
73
|
-
--
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
--
|
|
73
|
+
--port <number> Port to listen on (default: ${PROD_SERVER_PORT})
|
|
74
|
+
--host <host> Bind to a specific host or IP
|
|
75
|
+
--remote Shortcut for --host 0.0.0.0
|
|
76
|
+
--strict-port Fail instead of trying another port
|
|
77
|
+
--no-open Don't open browser automatically
|
|
78
|
+
--version Print version and exit
|
|
79
|
+
--help Show this help message`)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
80
83
|
let port = PROD_SERVER_PORT
|
|
84
|
+
let host = "127.0.0.1"
|
|
81
85
|
let openBrowser = true
|
|
82
86
|
let strictPort = false
|
|
83
87
|
|
|
@@ -96,6 +100,17 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
96
100
|
index += 1
|
|
97
101
|
continue
|
|
98
102
|
}
|
|
103
|
+
if (arg === "--host") {
|
|
104
|
+
const next = argv[index + 1]
|
|
105
|
+
if (!next || next.startsWith("-")) throw new Error("Missing value for --host")
|
|
106
|
+
host = next
|
|
107
|
+
index += 1
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if (arg === "--remote") {
|
|
111
|
+
host = "0.0.0.0"
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
99
114
|
if (arg === "--no-open") {
|
|
100
115
|
openBrowser = false
|
|
101
116
|
continue
|
|
@@ -111,6 +126,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
111
126
|
kind: "run",
|
|
112
127
|
options: {
|
|
113
128
|
port,
|
|
129
|
+
host,
|
|
114
130
|
openBrowser,
|
|
115
131
|
strictPort,
|
|
116
132
|
},
|
|
@@ -210,10 +226,11 @@ export async function runCli(argv: string[], deps: CliRuntimeDeps): Promise<CliR
|
|
|
210
226
|
command: CLI_COMMAND,
|
|
211
227
|
},
|
|
212
228
|
})
|
|
213
|
-
const
|
|
214
|
-
const
|
|
229
|
+
const bindHost = parsedArgs.options.host
|
|
230
|
+
const displayHost = bindHost === "127.0.0.1" || bindHost === "0.0.0.0" ? "localhost" : bindHost
|
|
231
|
+
const launchUrl = `http://${displayHost}:${port}`
|
|
215
232
|
|
|
216
|
-
deps.log(`${LOG_PREFIX} listening on
|
|
233
|
+
deps.log(`${LOG_PREFIX} listening on http://${bindHost}:${port}`)
|
|
217
234
|
deps.log(`${LOG_PREFIX} data dir: ${getDataDirDisplay()}`)
|
|
218
235
|
|
|
219
236
|
const suppressOpenBrowser = process.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] === "1"
|
|
@@ -56,6 +56,35 @@ describe("QuickResponseAdapter", () => {
|
|
|
56
56
|
|
|
57
57
|
expect(result).toBe("Codex title")
|
|
58
58
|
})
|
|
59
|
+
|
|
60
|
+
test("falls back to Codex when Claude throws", async () => {
|
|
61
|
+
const adapter = new QuickResponseAdapter({
|
|
62
|
+
runClaudeStructured: async () => {
|
|
63
|
+
throw new Error("Not authenticated")
|
|
64
|
+
},
|
|
65
|
+
runCodexStructured: async () => ({ title: "Codex title" }),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const result = await adapter.generateStructured({
|
|
69
|
+
cwd: "/tmp/project",
|
|
70
|
+
task: "title generation",
|
|
71
|
+
prompt: "Generate a title",
|
|
72
|
+
schema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
title: { type: "string" },
|
|
76
|
+
},
|
|
77
|
+
required: ["title"],
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
parse: (value) => {
|
|
81
|
+
const output = value && typeof value === "object" ? value as { title?: unknown } : {}
|
|
82
|
+
return typeof output.title === "string" ? output.title : null
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result).toBe("Codex title")
|
|
87
|
+
})
|
|
59
88
|
})
|
|
60
89
|
|
|
61
90
|
describe("generateTitleForChat", () => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk"
|
|
2
2
|
import { CodexAppServerManager } from "./codex-app-server"
|
|
3
3
|
|
|
4
|
+
const CLAUDE_STRUCTURED_TIMEOUT_MS = 5_000
|
|
5
|
+
|
|
4
6
|
type JsonSchema = {
|
|
5
7
|
type: "object"
|
|
6
8
|
properties: Record<string, unknown>
|
|
@@ -61,14 +63,31 @@ async function runClaudeStructured(args: Omit<StructuredQuickResponseArgs<unknow
|
|
|
61
63
|
})
|
|
62
64
|
|
|
63
65
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const result = await Promise.race<unknown | null>([
|
|
67
|
+
(async () => {
|
|
68
|
+
for await (const message of q) {
|
|
69
|
+
if ("result" in message) {
|
|
70
|
+
return (message as Record<string, unknown>).structured_output ?? null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
})(),
|
|
75
|
+
new Promise<null>((_, reject) => {
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
reject(new Error(`Claude structured response timed out after ${CLAUDE_STRUCTURED_TIMEOUT_MS}ms`))
|
|
78
|
+
}, CLAUDE_STRUCTURED_TIMEOUT_MS)
|
|
79
|
+
}),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
} catch (error) {
|
|
69
84
|
return null
|
|
70
85
|
} finally {
|
|
71
|
-
|
|
86
|
+
try {
|
|
87
|
+
q.close()
|
|
88
|
+
} catch {
|
|
89
|
+
// Ignore close failures on timed-out or failed quick responses.
|
|
90
|
+
}
|
|
72
91
|
}
|
|
73
92
|
}
|
|
74
93
|
|
|
@@ -95,7 +114,6 @@ export class QuickResponseAdapter {
|
|
|
95
114
|
this.runCodexStructured = args.runCodexStructured ?? ((structuredArgs) =>
|
|
96
115
|
runCodexStructured(this.codexManager, structuredArgs))
|
|
97
116
|
}
|
|
98
|
-
|
|
99
117
|
async generateStructured<T>(args: StructuredQuickResponseArgs<T>): Promise<T | null> {
|
|
100
118
|
const request = {
|
|
101
119
|
cwd: args.cwd,
|
|
@@ -104,20 +122,31 @@ export class QuickResponseAdapter {
|
|
|
104
122
|
schema: args.schema,
|
|
105
123
|
}
|
|
106
124
|
|
|
107
|
-
const claudeResult = await this.tryProvider(args.parse, () => this.runClaudeStructured(request))
|
|
125
|
+
const claudeResult = await this.tryProvider("claude", args.task, args.parse, () => this.runClaudeStructured(request))
|
|
108
126
|
if (claudeResult !== null) return claudeResult
|
|
109
127
|
|
|
110
|
-
return await this.tryProvider(args.parse, () => this.runCodexStructured(request))
|
|
128
|
+
return await this.tryProvider("codex", args.task, args.parse, () => this.runCodexStructured(request))
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
private async tryProvider<T>(
|
|
132
|
+
provider: "claude" | "codex",
|
|
133
|
+
task: string,
|
|
114
134
|
parse: (value: unknown) => T | null,
|
|
115
135
|
run: () => Promise<unknown | null>
|
|
116
136
|
): Promise<T | null> {
|
|
117
137
|
try {
|
|
118
138
|
const result = await run()
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
if (result === null) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parsed = parse(result)
|
|
144
|
+
if (parsed === null) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parsed
|
|
149
|
+
} catch (error) {
|
|
121
150
|
return null
|
|
122
151
|
}
|
|
123
152
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createWsRouter, type ClientState } from "./ws-router"
|
|
|
12
12
|
|
|
13
13
|
export interface StartKannaServerOptions {
|
|
14
14
|
port?: number
|
|
15
|
+
host?: string
|
|
15
16
|
strictPort?: boolean
|
|
16
17
|
update?: {
|
|
17
18
|
version: string
|
|
@@ -22,6 +23,7 @@ export interface StartKannaServerOptions {
|
|
|
22
23
|
|
|
23
24
|
export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
24
25
|
const port = options.port ?? 3210
|
|
26
|
+
const hostname = options.host ?? "127.0.0.1"
|
|
25
27
|
const strictPort = options.strictPort ?? false
|
|
26
28
|
const store = new EventStore()
|
|
27
29
|
const machineDisplayName = getMachineDisplayName()
|
|
@@ -74,6 +76,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
74
76
|
try {
|
|
75
77
|
server = Bun.serve<ClientState>({
|
|
76
78
|
port: actualPort,
|
|
79
|
+
hostname,
|
|
77
80
|
fetch(req, serverInstance) {
|
|
78
81
|
const url = new URL(req.url)
|
|
79
82
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_DEV_CLIENT_PORT,
|
|
4
|
+
getDefaultDevServerPort,
|
|
5
|
+
resolveDevPorts,
|
|
6
|
+
stripPortArg,
|
|
7
|
+
} from "./dev-ports"
|
|
8
|
+
|
|
9
|
+
describe("getDefaultDevServerPort", () => {
|
|
10
|
+
test("derives the default backend port from the client port", () => {
|
|
11
|
+
expect(getDefaultDevServerPort()).toBe(DEFAULT_DEV_CLIENT_PORT + 1)
|
|
12
|
+
expect(getDefaultDevServerPort(4000)).toBe(4001)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe("resolveDevPorts", () => {
|
|
17
|
+
test("uses default dev ports when no port override is provided", () => {
|
|
18
|
+
expect(resolveDevPorts([])).toEqual({
|
|
19
|
+
clientPort: DEFAULT_DEV_CLIENT_PORT,
|
|
20
|
+
serverPort: DEFAULT_DEV_CLIENT_PORT + 1,
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("treats --port as the client port and derives the backend port", () => {
|
|
25
|
+
expect(resolveDevPorts(["--remote", "--port", "4000"])).toEqual({
|
|
26
|
+
clientPort: 4000,
|
|
27
|
+
serverPort: 4001,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("uses the last provided --port value", () => {
|
|
32
|
+
expect(resolveDevPorts(["--port", "4000", "--port", "4100"])).toEqual({
|
|
33
|
+
clientPort: 4100,
|
|
34
|
+
serverPort: 4101,
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("throws when --port is missing a value", () => {
|
|
39
|
+
expect(() => resolveDevPorts(["--port"])).toThrow("Missing value for --port")
|
|
40
|
+
expect(() => resolveDevPorts(["--port", "--remote"])).toThrow("Missing value for --port")
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("stripPortArg", () => {
|
|
45
|
+
test("removes --port and its value while preserving other args", () => {
|
|
46
|
+
expect(stripPortArg(["--remote", "--port", "4000", "--host", "dev-box"])).toEqual([
|
|
47
|
+
"--remote",
|
|
48
|
+
"--host",
|
|
49
|
+
"dev-box",
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const DEFAULT_DEV_CLIENT_PORT = 5174
|
|
2
|
+
|
|
3
|
+
export function getDefaultDevServerPort(clientPort = DEFAULT_DEV_CLIENT_PORT) {
|
|
4
|
+
return clientPort + 1
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveDevPorts(args: string[]) {
|
|
8
|
+
let clientPort = DEFAULT_DEV_CLIENT_PORT
|
|
9
|
+
|
|
10
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
11
|
+
const arg = args[index]
|
|
12
|
+
if (arg !== "--port") continue
|
|
13
|
+
|
|
14
|
+
const next = args[index + 1]
|
|
15
|
+
if (!next || next.startsWith("-")) {
|
|
16
|
+
throw new Error("Missing value for --port")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
clientPort = Number(next)
|
|
20
|
+
index += 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
clientPort,
|
|
25
|
+
serverPort: getDefaultDevServerPort(clientPort),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stripPortArg(args: string[]) {
|
|
30
|
+
const stripped: string[] = []
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
33
|
+
const arg = args[index]
|
|
34
|
+
if (arg === "--port") {
|
|
35
|
+
index += 1
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stripped.push(arg)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return stripped
|
|
43
|
+
}
|
package/src/shared/ports.ts
CHANGED