kanna-code 0.3.0 → 0.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/index-5ura1eo0.js +419 -0
- package/dist/client/assets/index-B0Cwdy1-.css +1 -0
- package/dist/client/index.html +2 -2
- package/package.json +3 -2
- package/src/server/agent.test.ts +170 -0
- package/src/server/agent.ts +32 -0
- package/src/server/cli-runtime.test.ts +180 -0
- package/src/server/cli-runtime.ts +274 -0
- package/src/server/cli.ts +20 -127
- package/src/server/codex-app-server.test.ts +186 -0
- package/src/server/codex-app-server.ts +17 -2
- package/src/server/discovery.ts +0 -39
- package/src/server/server.ts +3 -1
- package/src/server/ws-router.test.ts +47 -0
- package/src/server/ws-router.ts +4 -0
- package/src/shared/protocol.ts +1 -0
- package/src/shared/tools.test.ts +12 -1
- package/src/shared/tools.ts +19 -1
- package/src/shared/types.ts +5 -1
- package/dist/client/assets/index-Byzgv_-q.js +0 -409
- package/dist/client/assets/index-gld9RxCU.css +0 -1
package/src/server/discovery.ts
CHANGED
|
@@ -4,8 +4,6 @@ import path from "node:path"
|
|
|
4
4
|
import type { AgentProvider } from "../shared/types"
|
|
5
5
|
import { resolveLocalPath } from "./paths"
|
|
6
6
|
|
|
7
|
-
const LOG_PREFIX = "[kanna discovery]"
|
|
8
|
-
|
|
9
7
|
export interface DiscoveredProject {
|
|
10
8
|
localPath: string
|
|
11
9
|
title: string
|
|
@@ -91,23 +89,18 @@ export class ClaudeProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
|
|
|
91
89
|
scan(homeDir: string = homedir()): ProviderDiscoveredProject[] {
|
|
92
90
|
const projectsDir = path.join(homeDir, ".claude", "projects")
|
|
93
91
|
if (!existsSync(projectsDir)) {
|
|
94
|
-
console.log(`${LOG_PREFIX} provider=claude status=missing root=${projectsDir}`)
|
|
95
92
|
return []
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
const entries = readdirSync(projectsDir, { withFileTypes: true })
|
|
99
96
|
const projects: ProviderDiscoveredProject[] = []
|
|
100
|
-
let directoryEntries = 0
|
|
101
|
-
let skippedMissing = 0
|
|
102
97
|
|
|
103
98
|
for (const entry of entries) {
|
|
104
99
|
if (!entry.isDirectory()) continue
|
|
105
|
-
directoryEntries += 1
|
|
106
100
|
|
|
107
101
|
const resolvedPath = resolveEncodedClaudePath(entry.name)
|
|
108
102
|
const normalizedPath = normalizeExistingDirectory(resolvedPath)
|
|
109
103
|
if (!normalizedPath) {
|
|
110
|
-
skippedMissing += 1
|
|
111
104
|
continue
|
|
112
105
|
}
|
|
113
106
|
|
|
@@ -125,10 +118,6 @@ export class ClaudeProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
|
|
|
125
118
|
...project,
|
|
126
119
|
}))
|
|
127
120
|
|
|
128
|
-
console.log(
|
|
129
|
-
`${LOG_PREFIX} provider=claude scanned=${directoryEntries} valid=${projects.length} deduped=${mergedProjects.length} skipped_missing=${skippedMissing} samples=${mergedProjects.slice(0, 5).map((project) => project.localPath).join(", ") || "-"}`
|
|
130
|
-
)
|
|
131
|
-
|
|
132
121
|
return mergedProjects
|
|
133
122
|
}
|
|
134
123
|
}
|
|
@@ -244,36 +233,19 @@ export class CodexProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
|
|
|
244
233
|
const metadataById = readCodexSessionMetadata(sessionsDir)
|
|
245
234
|
const configuredProjects = readCodexConfiguredProjects(configPath)
|
|
246
235
|
const projects: ProviderDiscoveredProject[] = []
|
|
247
|
-
let skippedMissingMeta = 0
|
|
248
|
-
let skippedRelative = 0
|
|
249
|
-
let skippedMissingPath = 0
|
|
250
|
-
let fallbackSessionTimestamps = 0
|
|
251
|
-
let configProjectsIncluded = 0
|
|
252
|
-
|
|
253
|
-
if (!existsSync(indexPath) || !existsSync(sessionsDir) || !existsSync(configPath)) {
|
|
254
|
-
console.log(
|
|
255
|
-
`${LOG_PREFIX} provider=codex status=missing index_exists=${existsSync(indexPath)} sessions_exists=${existsSync(sessionsDir)} config_exists=${existsSync(configPath)}`
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
236
|
|
|
259
237
|
for (const [sessionId, metadata] of metadataById.entries()) {
|
|
260
238
|
const modifiedAt = updatedAtById.get(sessionId) ?? metadata.modifiedAt
|
|
261
239
|
const cwd = metadata.cwd
|
|
262
|
-
if (!updatedAtById.has(sessionId)) {
|
|
263
|
-
fallbackSessionTimestamps += 1
|
|
264
|
-
}
|
|
265
240
|
if (!cwd) {
|
|
266
|
-
skippedMissingMeta += 1
|
|
267
241
|
continue
|
|
268
242
|
}
|
|
269
243
|
if (!path.isAbsolute(cwd)) {
|
|
270
|
-
skippedRelative += 1
|
|
271
244
|
continue
|
|
272
245
|
}
|
|
273
246
|
|
|
274
247
|
const normalizedPath = normalizeExistingDirectory(cwd)
|
|
275
248
|
if (!normalizedPath) {
|
|
276
|
-
skippedMissingPath += 1
|
|
277
249
|
continue
|
|
278
250
|
}
|
|
279
251
|
|
|
@@ -287,17 +259,14 @@ export class CodexProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
|
|
|
287
259
|
|
|
288
260
|
for (const [configuredPath, modifiedAt] of configuredProjects.entries()) {
|
|
289
261
|
if (!path.isAbsolute(configuredPath)) {
|
|
290
|
-
skippedRelative += 1
|
|
291
262
|
continue
|
|
292
263
|
}
|
|
293
264
|
|
|
294
265
|
const normalizedPath = normalizeExistingDirectory(configuredPath)
|
|
295
266
|
if (!normalizedPath) {
|
|
296
|
-
skippedMissingPath += 1
|
|
297
267
|
continue
|
|
298
268
|
}
|
|
299
269
|
|
|
300
|
-
configProjectsIncluded += 1
|
|
301
270
|
projects.push({
|
|
302
271
|
provider: this.provider,
|
|
303
272
|
localPath: normalizedPath,
|
|
@@ -311,10 +280,6 @@ export class CodexProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
|
|
|
311
280
|
...project,
|
|
312
281
|
}))
|
|
313
282
|
|
|
314
|
-
console.log(
|
|
315
|
-
`${LOG_PREFIX} provider=codex indexed_sessions=${updatedAtById.size} session_meta=${metadataById.size} config_projects=${configuredProjects.size} valid=${projects.length} deduped=${mergedProjects.length} fallback_session_timestamps=${fallbackSessionTimestamps} config_projects_included=${configProjectsIncluded} skipped_missing_meta=${skippedMissingMeta} skipped_relative=${skippedRelative} skipped_missing_path=${skippedMissingPath} samples=${mergedProjects.slice(0, 5).map((project) => project.localPath).join(", ") || "-"}`
|
|
316
|
-
)
|
|
317
|
-
|
|
318
283
|
return mergedProjects
|
|
319
284
|
}
|
|
320
285
|
}
|
|
@@ -332,9 +297,5 @@ export function discoverProjects(
|
|
|
332
297
|
adapters.flatMap((adapter) => adapter.scan(homeDir).map(({ provider: _provider, ...project }) => project))
|
|
333
298
|
)
|
|
334
299
|
|
|
335
|
-
console.log(
|
|
336
|
-
`${LOG_PREFIX} aggregate providers=${adapters.map((adapter) => adapter.provider).join(",")} total=${mergedProjects.length} samples=${mergedProjects.slice(0, 10).map((project) => project.localPath).join(", ") || "-"}`
|
|
337
|
-
)
|
|
338
|
-
|
|
339
300
|
return mergedProjects
|
|
340
301
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -8,10 +8,12 @@ import { createWsRouter, type ClientState } from "./ws-router"
|
|
|
8
8
|
|
|
9
9
|
export interface StartKannaServerOptions {
|
|
10
10
|
port?: number
|
|
11
|
+
strictPort?: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
14
15
|
const port = options.port ?? 3210
|
|
16
|
+
const strictPort = options.strictPort ?? false
|
|
15
17
|
const store = new EventStore()
|
|
16
18
|
const machineDisplayName = getMachineDisplayName()
|
|
17
19
|
await store.initialize()
|
|
@@ -83,7 +85,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
83
85
|
} catch (err: unknown) {
|
|
84
86
|
const isAddrInUse =
|
|
85
87
|
err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "EADDRINUSE"
|
|
86
|
-
if (!isAddrInUse || attempt === MAX_PORT_ATTEMPTS - 1) {
|
|
88
|
+
if (!isAddrInUse || strictPort || attempt === MAX_PORT_ATTEMPTS - 1) {
|
|
87
89
|
throw err
|
|
88
90
|
}
|
|
89
91
|
console.log(`Port ${actualPort} is in use, trying ${actualPort + 1}...`)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { PROTOCOL_VERSION } from "../shared/types"
|
|
3
|
+
import { createEmptyState } from "./events"
|
|
4
|
+
import { createWsRouter } from "./ws-router"
|
|
5
|
+
|
|
6
|
+
class FakeWebSocket {
|
|
7
|
+
readonly sent: unknown[] = []
|
|
8
|
+
readonly data = {
|
|
9
|
+
subscriptions: new Map(),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
send(message: string) {
|
|
13
|
+
this.sent.push(JSON.parse(message))
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("ws-router", () => {
|
|
18
|
+
test("acks system.ping without broadcasting snapshots", () => {
|
|
19
|
+
const router = createWsRouter({
|
|
20
|
+
store: { state: createEmptyState() } as never,
|
|
21
|
+
agent: { getActiveStatuses: () => new Map() } as never,
|
|
22
|
+
refreshDiscovery: async () => [],
|
|
23
|
+
getDiscoveredProjects: () => [],
|
|
24
|
+
machineDisplayName: "Local Machine",
|
|
25
|
+
})
|
|
26
|
+
const ws = new FakeWebSocket()
|
|
27
|
+
|
|
28
|
+
ws.data.subscriptions.set("sub-1", { type: "sidebar" })
|
|
29
|
+
router.handleMessage(
|
|
30
|
+
ws as never,
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
v: 1,
|
|
33
|
+
type: "command",
|
|
34
|
+
id: "ping-1",
|
|
35
|
+
command: { type: "system.ping" },
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(ws.sent).toEqual([
|
|
40
|
+
{
|
|
41
|
+
v: PROTOCOL_VERSION,
|
|
42
|
+
type: "ack",
|
|
43
|
+
id: "ping-1",
|
|
44
|
+
},
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
})
|
package/src/server/ws-router.ts
CHANGED
|
@@ -89,6 +89,10 @@ export function createWsRouter({
|
|
|
89
89
|
const { command, id } = message
|
|
90
90
|
try {
|
|
91
91
|
switch (command.type) {
|
|
92
|
+
case "system.ping": {
|
|
93
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
94
|
+
return
|
|
95
|
+
}
|
|
92
96
|
case "project.open": {
|
|
93
97
|
await ensureProjectDirectory(command.localPath)
|
|
94
98
|
const project = await store.openProject(command.localPath)
|
package/src/shared/protocol.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type ClientCommand =
|
|
|
9
9
|
| { type: "project.open"; localPath: string }
|
|
10
10
|
| { type: "project.create"; localPath: string; title: string }
|
|
11
11
|
| { type: "project.remove"; projectId: string }
|
|
12
|
+
| { type: "system.ping" }
|
|
12
13
|
| { type: "system.openExternal"; localPath: string; action: "open_finder" | "open_terminal" | "open_editor" }
|
|
13
14
|
| { type: "chat.create"; projectId: string }
|
|
14
15
|
| { type: "chat.rename"; chatId: string; title: string }
|
package/src/shared/tools.test.ts
CHANGED
|
@@ -62,7 +62,18 @@ describe("hydrateToolResult", () => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
const result = hydrateToolResult(tool, JSON.stringify({ answers: { runtime: "codex" } }))
|
|
65
|
-
expect(result).toEqual({ answers: { runtime: "codex" } })
|
|
65
|
+
expect(result).toEqual({ answers: { runtime: ["codex"] } })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("hydrates AskUserQuestion multi-select answers", () => {
|
|
69
|
+
const tool = normalizeToolCall({
|
|
70
|
+
toolName: "AskUserQuestion",
|
|
71
|
+
toolId: "tool-1",
|
|
72
|
+
input: { questions: [] },
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const result = hydrateToolResult(tool, JSON.stringify({ answers: { runtime: ["bun", "node"] } }))
|
|
76
|
+
expect(result).toEqual({ answers: { runtime: ["bun", "node"] } })
|
|
66
77
|
})
|
|
67
78
|
|
|
68
79
|
test("hydrates ExitPlanMode decisions", () => {
|
package/src/shared/tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AskUserQuestionItem,
|
|
3
|
+
AskUserQuestionAnswerMap,
|
|
3
4
|
AskUserQuestionToolResult,
|
|
4
5
|
ExitPlanModeToolResult,
|
|
5
6
|
HydratedToolCall,
|
|
@@ -209,7 +210,23 @@ export function hydrateToolResult(tool: NormalizedToolCall, raw: unknown): Hydra
|
|
|
209
210
|
case "ask_user_question": {
|
|
210
211
|
const record = asRecord(parsed)
|
|
211
212
|
const answers = asRecord(record?.answers) ?? (record ? record : {})
|
|
212
|
-
return {
|
|
213
|
+
return {
|
|
214
|
+
answers: Object.fromEntries(
|
|
215
|
+
Object.entries(answers).map(([key, value]) => {
|
|
216
|
+
if (Array.isArray(value)) {
|
|
217
|
+
return [key, value.map((entry) => String(entry))]
|
|
218
|
+
}
|
|
219
|
+
if (value && typeof value === "object" && Array.isArray((value as { answers?: unknown }).answers)) {
|
|
220
|
+
return [key, (value as { answers: unknown[] }).answers.map((entry) => String(entry))]
|
|
221
|
+
}
|
|
222
|
+
if (value == null || value === "") {
|
|
223
|
+
return [key, []]
|
|
224
|
+
}
|
|
225
|
+
return [key, [String(value)]]
|
|
226
|
+
})
|
|
227
|
+
) as AskUserQuestionAnswerMap,
|
|
228
|
+
...(record?.discarded === true ? { discarded: true } : {}),
|
|
229
|
+
} satisfies AskUserQuestionToolResult
|
|
213
230
|
}
|
|
214
231
|
case "exit_plan_mode": {
|
|
215
232
|
const record = asRecord(parsed)
|
|
@@ -217,6 +234,7 @@ export function hydrateToolResult(tool: NormalizedToolCall, raw: unknown): Hydra
|
|
|
217
234
|
confirmed: typeof record?.confirmed === "boolean" ? record.confirmed : undefined,
|
|
218
235
|
clearContext: typeof record?.clearContext === "boolean" ? record.clearContext : undefined,
|
|
219
236
|
message: typeof record?.message === "string" ? record.message : undefined,
|
|
237
|
+
...(record?.discarded === true ? { discarded: true } : {}),
|
|
220
238
|
} satisfies ExitPlanModeToolResult
|
|
221
239
|
}
|
|
222
240
|
case "read_file":
|
package/src/shared/types.ts
CHANGED
|
@@ -194,6 +194,8 @@ export interface AskUserQuestionItem {
|
|
|
194
194
|
multiSelect?: boolean
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
export type AskUserQuestionAnswerMap = Record<string, string[]>
|
|
198
|
+
|
|
197
199
|
export interface TodoItem {
|
|
198
200
|
content: string
|
|
199
201
|
status: "pending" | "in_progress" | "completed"
|
|
@@ -373,13 +375,15 @@ export interface HydratedToolCallBase<TKind extends string, TInput, TResult> {
|
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
export interface AskUserQuestionToolResult {
|
|
376
|
-
answers:
|
|
378
|
+
answers: AskUserQuestionAnswerMap
|
|
379
|
+
discarded?: boolean
|
|
377
380
|
}
|
|
378
381
|
|
|
379
382
|
export interface ExitPlanModeToolResult {
|
|
380
383
|
confirmed?: boolean
|
|
381
384
|
clearContext?: boolean
|
|
382
385
|
message?: string
|
|
386
|
+
discarded?: boolean
|
|
383
387
|
}
|
|
384
388
|
|
|
385
389
|
export type HydratedAskUserQuestionToolCall =
|