@100xprompt/brain 0.0.0-stage → 0.1.1
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/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { appendFileSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { exists, isDir, versionOf } from "../../fsutil.ts"
|
|
6
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
7
|
+
import { imageDataUrl } from "../../session/attachments.ts"
|
|
8
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
9
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
10
|
+
import { reader } from "./reader.ts"
|
|
11
|
+
import { LABEL, META_VERSION, TOOL, WIRE_PROTOCOL, workDirKey } from "./records.ts"
|
|
12
|
+
|
|
13
|
+
// Continues a portable session in Kimi Code as a new session of its own: sessions/<wd_slug_hash>/<id>/
|
|
14
|
+
// with agents/main/wire.jsonl and state.json, plus its line in session_index.jsonl. `kimi session list`
|
|
15
|
+
// lists it and `kimi --session <id>` resumes it.
|
|
16
|
+
//
|
|
17
|
+
// The records are the ones Kimi Code writes for a session converted from another tool (migration-legacy's
|
|
18
|
+
// writeMainAgentWire and writeImportedSession): per turn a turn.prompt, the turn's messages as
|
|
19
|
+
// context.append_message, and turn.ended, so the turn clock resumes past them. Unlike Kimi's own importer,
|
|
20
|
+
// which packs a foreign conversation into one message, every message stays its own: assistant messages
|
|
21
|
+
// carry their tool calls (Kimi's own tools where the arguments fit: Read, Write, Edit, Bash, Glob, Grep),
|
|
22
|
+
// each answered by a tool message, failures marked isError. Readable reasoning stays as think parts.
|
|
23
|
+
// The wire header is protocol 1.5, as Kimi Code 2.5.2 writes a new session; state.json is written last.
|
|
24
|
+
//
|
|
25
|
+
// The note saying where the session came from is a system reminder (origin kind "injection"), the way Kimi
|
|
26
|
+
// injects context it does not show as the person's words. A compaction is written as Kimi writes one:
|
|
27
|
+
// full_compaction.begin, context.apply_compaction and full_compaction.complete. The earlier turns stay on
|
|
28
|
+
// screen and the model starts from the summary.
|
|
29
|
+
|
|
30
|
+
/** Kimi Code versions whose session store this writer was checked against. */
|
|
31
|
+
export const TESTED_VERSIONS = ["2.5.2"]
|
|
32
|
+
|
|
33
|
+
/** The installed version (`kimi --version` prints "2.5.2"), or undefined when it is missing. */
|
|
34
|
+
export function kimiVersion(bin = "kimi"): string | undefined {
|
|
35
|
+
return versionOf(bin)?.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/)?.[1]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The note's injection variant; readers skip injections. */
|
|
39
|
+
export const NOTE_VARIANT = "brain_continuation"
|
|
40
|
+
|
|
41
|
+
const reminder = (text: string) => `<system-reminder>\n${text.trim()}\n</system-reminder>`
|
|
42
|
+
const continuedNote = (label: string, id: string, when: string) =>
|
|
43
|
+
reminder(
|
|
44
|
+
`This session continues ${label} session ${id}, carried over by brain session on ${when}. The turns below are its history; anything listed as lost did not carry over. Check files before relying on details, since they may have changed.`,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
48
|
+
|
|
49
|
+
/** Kimi records the folder its process runs in, which is the resolved path (/private/tmp, not /tmp). */
|
|
50
|
+
function realCwd(cwd: string): string {
|
|
51
|
+
try {
|
|
52
|
+
return realpathSync(cwd)
|
|
53
|
+
} catch {
|
|
54
|
+
return cwd
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
59
|
+
type Json = Record<string, unknown>
|
|
60
|
+
type Part = { type: "text"; text: string } | { type: "think"; think: string } | { type: "image_url"; imageUrl: { url: string } }
|
|
61
|
+
type Message =
|
|
62
|
+
| { role: "user"; content: Part[]; toolCalls: []; origin: Json }
|
|
63
|
+
| { role: "assistant"; content: Part[]; toolCalls: Array<{ type: "function"; id: string; name: string; arguments: string }> }
|
|
64
|
+
| { role: "tool"; content: Part[]; toolCalls: []; toolCallId: string; isError?: true }
|
|
65
|
+
|
|
66
|
+
// Argument names other tools use for the same thing.
|
|
67
|
+
const FILE = ["file_path", "path", "filePath", "absolute_path", "target_file", "file", "filename"]
|
|
68
|
+
const OLD = ["old_string", "oldString", "old_str", "old_text"]
|
|
69
|
+
const NEW = ["new_string", "newString", "new_str", "new_text"]
|
|
70
|
+
const CONTENT = ["content", "contents", "file_text", "text"]
|
|
71
|
+
const COMMAND = ["command", "cmd", "commandLine"]
|
|
72
|
+
const PATTERN = ["pattern", "regex", "query", "glob_pattern"]
|
|
73
|
+
const DIR = ["path", "dir_path", "directory", "target_directory"]
|
|
74
|
+
|
|
75
|
+
/** The call as Kimi Code's own tool when its arguments fit one; otherwise undefined and it keeps its own. */
|
|
76
|
+
export function kimiTool(c: Call): { name: string; args: Json } | undefined {
|
|
77
|
+
const input = c.input && typeof c.input === "object" && !Array.isArray(c.input) ? (c.input as Json) : {}
|
|
78
|
+
const str = (keys: string[]) => {
|
|
79
|
+
const v = keys.map((k) => input[k]).find((x) => typeof x === "string" || (Array.isArray(x) && x.every((y) => typeof y === "string")))
|
|
80
|
+
return Array.isArray(v) ? v.join(" ") : (v as string | undefined)
|
|
81
|
+
}
|
|
82
|
+
const path = str(FILE)
|
|
83
|
+
switch (c.toolKind) {
|
|
84
|
+
case "read":
|
|
85
|
+
return path ? { name: "Read", args: { path } } : undefined
|
|
86
|
+
case "edit": {
|
|
87
|
+
const [o, n] = [str(OLD), str(NEW)]
|
|
88
|
+
return path && o && n !== undefined ? { name: "Edit", args: { path, old_string: o, new_string: n } } : undefined
|
|
89
|
+
}
|
|
90
|
+
case "write": {
|
|
91
|
+
const content = str(CONTENT)
|
|
92
|
+
return path && content !== undefined ? { name: "Write", args: { path, content } } : undefined
|
|
93
|
+
}
|
|
94
|
+
case "shell": {
|
|
95
|
+
const command = str(COMMAND)
|
|
96
|
+
return command ? { name: "Bash", args: { command } } : undefined
|
|
97
|
+
}
|
|
98
|
+
case "search": {
|
|
99
|
+
const pattern = str(PATTERN)
|
|
100
|
+
const dir = str(DIR)
|
|
101
|
+
return pattern ? { name: /glob/i.test(c.name) ? "Glob" : "Grep", args: { pattern, ...(dir ? { path: dir } : {}) } } : undefined
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WirePlan {
|
|
109
|
+
records: Json[]
|
|
110
|
+
lost: string[]
|
|
111
|
+
/** What a read-back must find. */
|
|
112
|
+
expect: { users: number; assistants: number; calls: number; compactions: number }
|
|
113
|
+
title: string
|
|
114
|
+
lastPrompt: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The wire.jsonl records of a new Kimi Code session continuing `session`. Pure. */
|
|
118
|
+
export function planWire(session: Session, opts: { now: Date }): WirePlan {
|
|
119
|
+
const drops = new Map<string, number>()
|
|
120
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
121
|
+
const label = toolLabel(session.header.tool)
|
|
122
|
+
let clock = opts.now.getTime()
|
|
123
|
+
const time = () => clock++
|
|
124
|
+
const records: Json[] = [{ type: "metadata", protocol_version: WIRE_PROTOCOL, created_at: opts.now.getTime() }]
|
|
125
|
+
const expect = { users: 0, assistants: 0, calls: 0, compactions: 0 }
|
|
126
|
+
|
|
127
|
+
// The turns as Kimi groups them: each user message opens a turn (turn.prompt … turn.ended).
|
|
128
|
+
let turnId = 0
|
|
129
|
+
let inTurn = false
|
|
130
|
+
let replied = false
|
|
131
|
+
// Messages in the model's context since the last compaction, which a compaction record counts.
|
|
132
|
+
let context = 0
|
|
133
|
+
const append = (message: Message) => {
|
|
134
|
+
records.push({ type: "context.append_message", agentId: "main", message, time: time() })
|
|
135
|
+
context++
|
|
136
|
+
}
|
|
137
|
+
const endTurn = () => {
|
|
138
|
+
if (inTurn && replied) records.push({ type: "turn.ended", agentId: "main", turnId, reason: "completed", time: time() })
|
|
139
|
+
if (inTurn) turnId++
|
|
140
|
+
inTurn = false
|
|
141
|
+
replied = false
|
|
142
|
+
}
|
|
143
|
+
const openTurn = (input: Part[], origin: Json) => {
|
|
144
|
+
endTurn()
|
|
145
|
+
records.push({ type: "turn.prompt", agentId: "main", input, origin, time: time() })
|
|
146
|
+
inTurn = true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Call ids stay as they were when Kimi can carry them, else get a call_ id; each is used once.
|
|
150
|
+
const ids = new Map<string, string>()
|
|
151
|
+
const used = new Set<string>()
|
|
152
|
+
const idFor = (id: string) => {
|
|
153
|
+
let v = ids.get(id)
|
|
154
|
+
if (v) return v
|
|
155
|
+
const base = /^[A-Za-z0-9_.:-]{1,64}$/.test(id) ? id : `call_${id.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
|
|
156
|
+
v = base
|
|
157
|
+
for (let n = 2; used.has(v); n++) v = `${base}_${n}`
|
|
158
|
+
ids.set(id, v)
|
|
159
|
+
used.add(v)
|
|
160
|
+
return v
|
|
161
|
+
}
|
|
162
|
+
const results = new Map<string, Extract<Turn, { kind: "tool_result" }>>()
|
|
163
|
+
for (const t of session.turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
164
|
+
|
|
165
|
+
// The assistant message being built: its reasoning, text and calls.
|
|
166
|
+
let cur: { think: string[]; text: string; calls: Call[] } | undefined
|
|
167
|
+
let pendingThink: string[] = []
|
|
168
|
+
const answered = new Set<string>()
|
|
169
|
+
const flush = () => {
|
|
170
|
+
if (!cur) return
|
|
171
|
+
const content: Part[] = [...cur.think.map((think) => ({ type: "think" as const, think })), ...(cur.text ? [{ type: "text" as const, text: cur.text }] : [])]
|
|
172
|
+
const toolCalls = cur.calls.map((c) => {
|
|
173
|
+
const own = kimiTool(c)
|
|
174
|
+
return { type: "function" as const, id: idFor(c.id), name: own?.name ?? (c.name.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 64) || "tool"), arguments: JSON.stringify(own?.args ?? c.input ?? {}) }
|
|
175
|
+
})
|
|
176
|
+
if (!inTurn) openTurn([], { kind: "system_trigger", name: "imported_orphan" })
|
|
177
|
+
append({ role: "assistant", content, toolCalls })
|
|
178
|
+
replied = true
|
|
179
|
+
if (cur.text.trim()) expect.assistants++
|
|
180
|
+
// A call the session never answered is closed as Kimi closes an interrupted one.
|
|
181
|
+
for (const c of cur.calls) {
|
|
182
|
+
if (results.has(c.id)) continue
|
|
183
|
+
append({ role: "tool", content: [{ type: "text", text: "Tool execution was interrupted before its result was recorded. Do not assume the tool completed successfully." }], toolCalls: [], toolCallId: idFor(c.id), isError: true })
|
|
184
|
+
answered.add(c.id)
|
|
185
|
+
drop("tool calls without a result (closed as interrupted)")
|
|
186
|
+
}
|
|
187
|
+
cur = undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const firstUser = session.turns.findIndex((t) => t.kind === "user" && t.text.trim())
|
|
191
|
+
const note: Message = { role: "user", content: [{ type: "text", text: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)) }], toolCalls: [], origin: { kind: "injection", variant: NOTE_VARIANT } }
|
|
192
|
+
if (firstUser < 0) {
|
|
193
|
+
openTurn([], { kind: "system_trigger", name: "imported_orphan" })
|
|
194
|
+
append(note)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const [i, t] of session.turns.entries()) {
|
|
198
|
+
switch (t.kind) {
|
|
199
|
+
case "user": {
|
|
200
|
+
flush()
|
|
201
|
+
if (pendingThink.length) drop("reasoning without a reply", pendingThink.length)
|
|
202
|
+
pendingThink = []
|
|
203
|
+
// Kimi keeps a pasted image in the message as an image_url part holding a data URL.
|
|
204
|
+
const images: Part[] = []
|
|
205
|
+
for (const a of t.attachments ?? []) {
|
|
206
|
+
const url = imageDataUrl(a)
|
|
207
|
+
if (url) images.push({ type: "image_url", imageUrl: { url } })
|
|
208
|
+
else drop("attachments")
|
|
209
|
+
}
|
|
210
|
+
if (!t.text.trim()) {
|
|
211
|
+
if (images.length) drop("attachments without text", images.length)
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
const content: Part[] = [{ type: "text", text: t.text }, ...images]
|
|
215
|
+
openTurn(content, { kind: "user" })
|
|
216
|
+
// Kimi puts the context it injects for a prompt just before the prompt's message.
|
|
217
|
+
if (i === firstUser) append(note)
|
|
218
|
+
append({ role: "user", content, toolCalls: [], origin: { kind: "user" } })
|
|
219
|
+
expect.users++
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
case "assistant":
|
|
223
|
+
if (cur?.calls.length || (cur?.text.trim() && t.text.trim())) flush()
|
|
224
|
+
cur ??= { think: pendingThink, text: "", calls: [] }
|
|
225
|
+
pendingThink = []
|
|
226
|
+
cur.text = t.text
|
|
227
|
+
break
|
|
228
|
+
case "reasoning":
|
|
229
|
+
if (cur?.calls.length || cur?.text.trim()) flush()
|
|
230
|
+
pendingThink.push(t.text)
|
|
231
|
+
break
|
|
232
|
+
case "tool_call":
|
|
233
|
+
if (ids.has(t.id)) {
|
|
234
|
+
drop("repeated tool call ids")
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
idFor(t.id)
|
|
238
|
+
cur ??= { think: pendingThink, text: "", calls: [] }
|
|
239
|
+
pendingThink = []
|
|
240
|
+
cur.calls.push(t)
|
|
241
|
+
expect.calls++
|
|
242
|
+
break
|
|
243
|
+
case "tool_result": {
|
|
244
|
+
if (!ids.has(t.callId)) {
|
|
245
|
+
drop("tool results without a call")
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
if (answered.has(t.callId) || results.get(t.callId) !== t) {
|
|
249
|
+
drop("repeated tool results")
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
if (cur?.calls.some((c) => c.id === t.callId)) flush()
|
|
253
|
+
let text = t.output
|
|
254
|
+
if (t.fullPath) text += `\n\n[Full output: ${t.fullPath}]`
|
|
255
|
+
// Kimi renders an empty output as "Tool output is empty."; an empty text part would be dropped.
|
|
256
|
+
append({ role: "tool", content: [{ type: "text", text: text || (t.ok ? "Tool output is empty." : "The tool failed.") }], toolCalls: [], toolCallId: idFor(t.callId), ...(t.ok ? {} : { isError: true as const }) })
|
|
257
|
+
answered.add(t.callId)
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
case "compaction": {
|
|
261
|
+
flush()
|
|
262
|
+
endTurn()
|
|
263
|
+
const summary = `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`
|
|
264
|
+
// compactedCount is the whole context so far, so the model starts from the summary alone.
|
|
265
|
+
records.push({ type: "full_compaction.begin", agentId: "main", source: "auto", time: time() })
|
|
266
|
+
records.push({ type: "context.apply_compaction", agentId: "main", summary, contextSummary: summary, compactedCount: context, tokensBefore: 0, time: time() })
|
|
267
|
+
records.push({ type: "full_compaction.complete", agentId: "main", time: time() })
|
|
268
|
+
context = 1
|
|
269
|
+
expect.compactions++
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
case "file_edit":
|
|
273
|
+
if (!t.callId || !ids.has(t.callId)) drop("file edits outside a tool call")
|
|
274
|
+
break
|
|
275
|
+
case "subagent":
|
|
276
|
+
drop("subagent transcripts (their results stay in the parent's tool results)")
|
|
277
|
+
break
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
flush()
|
|
281
|
+
if (pendingThink.length) drop("reasoning without a reply", pendingThink.length)
|
|
282
|
+
endTurn()
|
|
283
|
+
|
|
284
|
+
const title = `${session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"} (from ${label})`
|
|
285
|
+
const lastUser = [...session.turns].reverse().find((t) => t.kind === "user" && t.text.trim())
|
|
286
|
+
return { records, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect, title, lastPrompt: lastUser?.kind === "user" ? lastUser.text.slice(0, 200) : "" }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The Kimi Code writer; `version` is for tests, which must not depend on an installed binary. */
|
|
290
|
+
export function kimiCodeWriter(opts: { version?: string } = {}): Writer {
|
|
291
|
+
const installed = () => opts.version ?? kimiVersion()
|
|
292
|
+
return {
|
|
293
|
+
tool: TOOL,
|
|
294
|
+
label: LABEL,
|
|
295
|
+
way: "native",
|
|
296
|
+
|
|
297
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
298
|
+
const v = installed()
|
|
299
|
+
if (!v) return { ok: false, why: "Kimi Code is not installed (`kimi --version` did not run)" }
|
|
300
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Kimi Code ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
301
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Kimi Code finds a session by its working directory` }
|
|
302
|
+
return { ok: true }
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
306
|
+
const cwd = realCwd(w.cwd)
|
|
307
|
+
const home = join(w.home, ".kimi-code")
|
|
308
|
+
const bucket = join(home, "sessions", workDirKey(cwd))
|
|
309
|
+
let sessionId = `session_${randomUUID()}`
|
|
310
|
+
while (exists(join(bucket, sessionId))) sessionId = `session_${randomUUID()}`
|
|
311
|
+
const dir = join(bucket, sessionId)
|
|
312
|
+
const now = new Date()
|
|
313
|
+
const plan = planWire(session, { now })
|
|
314
|
+
const paths = { wire: join(dir, "agents", "main", "wire.jsonl"), state: join(dir, "state.json"), index: join(home, "session_index.jsonl") }
|
|
315
|
+
if (!w.dryRun) {
|
|
316
|
+
mkdirSync(bucket, { recursive: true, mode: 0o700 })
|
|
317
|
+
// mkdir without recursive on the session folder itself: an existing session is never touched.
|
|
318
|
+
mkdirSync(dir, { mode: 0o700 })
|
|
319
|
+
try {
|
|
320
|
+
mkdirSync(join(dir, "agents", "main"), { recursive: true, mode: 0o700 })
|
|
321
|
+
writeFileSync(paths.wire, `${plan.records.map((r) => JSON.stringify(r)).join("\n")}\n`, { flag: "wx", mode: 0o600 })
|
|
322
|
+
const at = now.getTime()
|
|
323
|
+
const state = {
|
|
324
|
+
id: sessionId,
|
|
325
|
+
version: META_VERSION,
|
|
326
|
+
cwd,
|
|
327
|
+
createdAt: at,
|
|
328
|
+
updatedAt: at,
|
|
329
|
+
archived: false,
|
|
330
|
+
title: plan.title,
|
|
331
|
+
// "generated" keeps the title when the session goes on, as Kimi does for imported sessions.
|
|
332
|
+
titleKind: "generated",
|
|
333
|
+
isCustomTitle: false,
|
|
334
|
+
lastPrompt: plan.lastPrompt,
|
|
335
|
+
lastTurnReason: "completed",
|
|
336
|
+
agents: { main: { homedir: join(dir, "agents", "main"), type: "main", parentAgentId: null } },
|
|
337
|
+
custom: { continued_from: { tool: session.header.tool, id: session.header.sourceId, by: "brain session" } },
|
|
338
|
+
}
|
|
339
|
+
writeFileSync(paths.state, JSON.stringify(state, null, 2), { flag: "wx", mode: 0o600 })
|
|
340
|
+
const back = await reader.read({ tool: TOOL, sourceId: sessionId, path: paths.wire, extra: { dir } }, w.home)
|
|
341
|
+
const n = countTurns(back)
|
|
342
|
+
const pairs = unpaired(back)
|
|
343
|
+
if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || n.compaction !== plan.expect.compactions || pairs.calls.length || pairs.results.length)
|
|
344
|
+
throw new Error(
|
|
345
|
+
`the new Kimi Code session did not read back as written (users ${n.user}/${plan.expect.users}, replies ${n.assistant}/${plan.expect.assistants}, tool calls ${n.tool_call}/${plan.expect.calls}, compactions ${n.compaction}/${plan.expect.compactions}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
|
|
346
|
+
)
|
|
347
|
+
} catch (e) {
|
|
348
|
+
rmSync(dir, { recursive: true, force: true })
|
|
349
|
+
throw e
|
|
350
|
+
}
|
|
351
|
+
// The index Kimi uses to find a session by id alone; one appended line, as Kimi's own writers add.
|
|
352
|
+
appendFileSync(paths.index, `${JSON.stringify({ sessionId, sessionDir: dir, workDir: cwd })}\n`, { mode: 0o600 })
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
tool: TOOL,
|
|
356
|
+
way: "native",
|
|
357
|
+
sessionId,
|
|
358
|
+
written: [paths.wire, paths.state, paths.index],
|
|
359
|
+
open: `cd ${shellQuote(cwd)} && kimi --session ${sessionId}`,
|
|
360
|
+
lost: plan.lost,
|
|
361
|
+
dryRun: !!w.dryRun,
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export const kimiCodeNativeWriter = kimiCodeWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { kiroCliReader } from "./reader.ts"
|
|
3
|
+
import { kiroCliNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Kiro CLI (successor to the Amazon Q Developer CLI): reads Kiro's V3 session store, which Kiro IDE shares,
|
|
6
|
+
// and continues any session there as a new Kiro session that `kiro-cli chat --resume-id` opens.
|
|
7
|
+
export const readers: Reader[] = [kiroCliReader]
|
|
8
|
+
export const writers: Writer[] = [kiroCliNativeWriter]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { gitInfo, isDir, ls, readJson, readJsonl } from "../../fsutil.ts"
|
|
3
|
+
import type { Session } from "../../session/types.ts"
|
|
4
|
+
import { header, iso, noteModel } from "../../session/util.ts"
|
|
5
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
6
|
+
import { type KiroMetadata, type KiroRow, sessionsDir, turnsOf } from "./records.ts"
|
|
7
|
+
|
|
8
|
+
// Kiro's V3 session store, ~/.kiro/sessions/<bucket>/<id>/, which Kiro CLI (3.0 and later, by default) and
|
|
9
|
+
// Kiro IDE (1.0 and later) share. A session's folder counts when it has a session.json, as Kiro's own lister
|
|
10
|
+
// requires; the bucket's ".index" folder is Kiro's cache of the list, not a session.
|
|
11
|
+
|
|
12
|
+
export const TOOL = "kiro-cli"
|
|
13
|
+
export const LABEL = "Kiro"
|
|
14
|
+
|
|
15
|
+
/** A session folder's metadata, or undefined when it is not one Kiro would list. */
|
|
16
|
+
function metadataOf(dir: string): KiroMetadata | undefined {
|
|
17
|
+
const m = readJson<KiroMetadata>(join(dir, "session.json"))
|
|
18
|
+
if (!m || typeof m.id !== "string" || typeof m.title !== "string" || !Array.isArray(m.workspacePaths)) return undefined
|
|
19
|
+
return m
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Every session folder in the store, with its metadata. */
|
|
23
|
+
export function sessionFolders(home: string): Array<{ dir: string; meta: KiroMetadata }> {
|
|
24
|
+
const out: Array<{ dir: string; meta: KiroMetadata }> = []
|
|
25
|
+
const base = sessionsDir(home)
|
|
26
|
+
for (const bucket of ls(base)) {
|
|
27
|
+
if (bucket.startsWith(".") || !isDir(join(base, bucket))) continue
|
|
28
|
+
for (const name of ls(join(base, bucket))) {
|
|
29
|
+
if (name.startsWith(".")) continue
|
|
30
|
+
const dir = join(base, bucket, name)
|
|
31
|
+
const meta = isDir(dir) ? metadataOf(dir) : undefined
|
|
32
|
+
if (meta) out.push({ dir, meta })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One session folder as a portable session. */
|
|
39
|
+
export function readKiroDir(dir: string, home: string, tool = TOOL): Session {
|
|
40
|
+
const meta = metadataOf(dir)
|
|
41
|
+
if (!meta) throw new Error(`${dir} is not a Kiro session (no readable session.json)`)
|
|
42
|
+
const { rows, bad } = readJsonl(join(dir, "messages.jsonl"))
|
|
43
|
+
// Kiro skips a line its record schema rejects; these are the lines it would not even parse.
|
|
44
|
+
const records = rows.filter((r): r is KiroRow => typeof r.id === "string" && typeof r.timestamp === "string" && typeof r.payload?.type === "string")
|
|
45
|
+
const cwd = meta.workspacePaths[0]
|
|
46
|
+
const h = header(tool, meta.id, {
|
|
47
|
+
sourcePath: dir,
|
|
48
|
+
title: meta.title,
|
|
49
|
+
project: cwd ? { cwd, ...gitInfo(cwd, home) } : {},
|
|
50
|
+
...(iso(meta.createdAt) ? { startedAt: iso(meta.createdAt)! } : {}),
|
|
51
|
+
...(iso(meta.lastModifiedAt) ? { updatedAt: iso(meta.lastModifiedAt)! } : {}),
|
|
52
|
+
})
|
|
53
|
+
noteModel(h, meta.modelId)
|
|
54
|
+
if (meta.parentSessionId) h.parent = { id: `${tool}:${meta.parentSessionId}`, kind: "fork" }
|
|
55
|
+
const { turns, continued } = turnsOf(records, h)
|
|
56
|
+
if (bad + rows.length - records.length) h.lost.push(`unreadable records (${bad + rows.length - records.length})`)
|
|
57
|
+
if (continued) h.parent = { id: continued, kind: "continued" }
|
|
58
|
+
return { header: h, turns }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A reader of the shared store under one tool id. */
|
|
62
|
+
export function kiroReader(tool: string, label: string): Reader {
|
|
63
|
+
return {
|
|
64
|
+
tool,
|
|
65
|
+
label,
|
|
66
|
+
async list(home) {
|
|
67
|
+
const refs: SessionRef[] = sessionFolders(home).map(({ dir, meta }) => {
|
|
68
|
+
const updatedAt = iso(meta.lastModifiedAt)
|
|
69
|
+
return {
|
|
70
|
+
tool,
|
|
71
|
+
sourceId: meta.id,
|
|
72
|
+
path: dir,
|
|
73
|
+
title: meta.title,
|
|
74
|
+
...(meta.workspacePaths[0] ? { cwd: meta.workspacePaths[0] } : {}),
|
|
75
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
79
|
+
},
|
|
80
|
+
async read(ref, home) {
|
|
81
|
+
if (!isDir(ref.path)) throw new Error(`Kiro session folder not found: ${ref.path}`)
|
|
82
|
+
return readKiroDir(ref.path, home, tool)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const kiroCliReader = kiroReader(TOOL, LABEL)
|