@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,260 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { join, posix, resolve } from "node:path"
|
|
3
|
+
import { versionOf } from "../../fsutil.ts"
|
|
4
|
+
import type { Session, ToolKind, Turn } from "../../session/types.ts"
|
|
5
|
+
import { asText, clip, iso, lossCounter, toolKindOf } from "../../session/util.ts"
|
|
6
|
+
import { CONTINUED_NOTE } from "../claude-code/records.ts"
|
|
7
|
+
|
|
8
|
+
// What Kiro's V3 sessions look like, shared by the Kiro CLI and Kiro IDE adapters: the V3 engine (Kiro's
|
|
9
|
+
// agent server, @kiro/agent) keeps both products' sessions in one store. Checked against @kiro/agent 0.66.91
|
|
10
|
+
// as Kiro CLI 3.1.0 ships it: SessionPersistence (saveSession, loadSession, listSessions, the bucket list
|
|
11
|
+
// index), the record schemas (session.json and the messages.jsonl line union), messagesToContextMessages
|
|
12
|
+
// (what the model is sent) and convertMessage (what the chat shows). Catalogue:
|
|
13
|
+
// docs/sessions/2026-11-08-session-formats.md, "Kiro CLI" and "Kiro IDE".
|
|
14
|
+
//
|
|
15
|
+
// A session is ~/.kiro/sessions/<bucket>/<id>/: session.json (metadata, rewritten whole) and messages.jsonl
|
|
16
|
+
// (one {id, timestamp, payload} record per line, appended). The bucket is the first 16 hex of the SHA-256 of
|
|
17
|
+
// the session's sorted, normalized workspace folders; the id is sess_<uuid>. Kiro lists a workspace's
|
|
18
|
+
// sessions from its bucket, through an index (<bucket>/.index/index.json) that re-walks the bucket when the
|
|
19
|
+
// folder's mtime changes, so a session folder added from outside is listed like one of Kiro's own.
|
|
20
|
+
|
|
21
|
+
export const TESTED_VERSIONS = ["3.1.0"]
|
|
22
|
+
/** The agent engine those versions ship; the record shapes below are its own. */
|
|
23
|
+
export const ENGINE = "@kiro/agent 0.66.91"
|
|
24
|
+
|
|
25
|
+
export const sessionsDir = (home: string) => join(home, ".kiro", "sessions")
|
|
26
|
+
|
|
27
|
+
/** "3.1.0" from `kiro-cli --version` ("kiro-cli 3.1.0"); undefined when kiro-cli is missing. */
|
|
28
|
+
export const kiroCliVersion = (bin = "kiro-cli") => versionOf(bin)?.match(/\d+\.\d+\.\d+/)?.[0]
|
|
29
|
+
|
|
30
|
+
/** Kiro's normalizePath: absolute, forward slashes, POSIX-normalized, no trailing slash. */
|
|
31
|
+
function normalizePath(p: string): string {
|
|
32
|
+
let n = posix.normalize(resolve(p).replace(/\\/g, "/"))
|
|
33
|
+
if (n.length > 1) n = n.replace(/\/+$/, "") || n
|
|
34
|
+
return n
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Kiro's computeWorkspaceHash: the bucket a session with these workspace folders lives in. */
|
|
38
|
+
export function workspaceHash(paths: string[]): string {
|
|
39
|
+
if (!paths.length) return "_global"
|
|
40
|
+
const key = paths.map(normalizePath).sort().join("\0")
|
|
41
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Kiro's session id rule (isValidSessionId): it refuses to load anything else. */
|
|
45
|
+
export const validSessionId = (id: string) => id.length > 0 && id.length <= 128 && /^[A-Za-z0-9_-]+$/.test(id)
|
|
46
|
+
|
|
47
|
+
/** session.json: the fields Kiro requires, and the optional ones this adapter reads or writes. */
|
|
48
|
+
export interface KiroMetadata {
|
|
49
|
+
schemaVersion: string
|
|
50
|
+
dataModelVersion?: number
|
|
51
|
+
id: string
|
|
52
|
+
title: string
|
|
53
|
+
agentMode: string
|
|
54
|
+
workspacePaths: string[]
|
|
55
|
+
createdAt: string
|
|
56
|
+
lastModifiedAt: string
|
|
57
|
+
modelId?: string
|
|
58
|
+
parentSessionId?: string
|
|
59
|
+
status?: string
|
|
60
|
+
titleSetByUser?: boolean
|
|
61
|
+
[k: string]: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One messages.jsonl line. */
|
|
65
|
+
export interface KiroRow {
|
|
66
|
+
id: string
|
|
67
|
+
timestamp: string
|
|
68
|
+
payload: Record<string, any> & { type: string }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Kiro's placeholder for reasoning it did not keep. */
|
|
72
|
+
const ELIDED_REASONING = "..."
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What a Kiro tool does. The V3 built-ins are read_file, fs_write, fs_append, str_replace, delete_file,
|
|
76
|
+
* list_directory, execute_bash (execute_pwsh on Windows), grep_search, file_search, web_fetch and
|
|
77
|
+
* invoke_sub_agent; MCP tools are named after their server.
|
|
78
|
+
*/
|
|
79
|
+
export function kiroToolKind(name: string): ToolKind {
|
|
80
|
+
switch (name) {
|
|
81
|
+
case "read_file":
|
|
82
|
+
return "read"
|
|
83
|
+
case "fs_write":
|
|
84
|
+
case "fs_append":
|
|
85
|
+
return "write"
|
|
86
|
+
case "str_replace":
|
|
87
|
+
case "delete_file":
|
|
88
|
+
return "edit"
|
|
89
|
+
case "list_directory":
|
|
90
|
+
return "list"
|
|
91
|
+
case "execute_bash":
|
|
92
|
+
case "execute_pwsh":
|
|
93
|
+
return "shell"
|
|
94
|
+
case "grep_search":
|
|
95
|
+
case "file_search":
|
|
96
|
+
return "search"
|
|
97
|
+
case "web_fetch":
|
|
98
|
+
return "web"
|
|
99
|
+
case "invoke_sub_agent":
|
|
100
|
+
return "task"
|
|
101
|
+
}
|
|
102
|
+
return toolKindOf(name)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const str = (v: unknown) => (typeof v === "string" ? v : undefined)
|
|
106
|
+
|
|
107
|
+
/** A call as Kiro's own built-in tool would have been called, when its arguments say what that tool needs. */
|
|
108
|
+
export function kiroCall(c: Extract<Turn, { kind: "tool_call" }>): { name: string; args: Record<string, unknown> } | undefined {
|
|
109
|
+
const i = (c.input && typeof c.input === "object" && !Array.isArray(c.input) ? c.input : {}) as Record<string, unknown>
|
|
110
|
+
const path = str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path)
|
|
111
|
+
switch (c.toolKind) {
|
|
112
|
+
case "shell": {
|
|
113
|
+
const command = str(i.command) ?? str(i.cmd) ?? (Array.isArray(i.command) ? (i.command as unknown[]).map(String).join(" ") : undefined)
|
|
114
|
+
return command ? { name: "execute_bash", args: { command } } : undefined
|
|
115
|
+
}
|
|
116
|
+
case "write": {
|
|
117
|
+
const text = str(i.content) ?? str(i.file_text) ?? str(i.contents) ?? str(i.text)
|
|
118
|
+
return path && text !== undefined ? { name: "fs_write", args: { path, text } } : undefined
|
|
119
|
+
}
|
|
120
|
+
case "edit": {
|
|
121
|
+
const oldStr = str(i.old_string) ?? str(i.old_str) ?? str(i.oldText) ?? str(i.oldStr)
|
|
122
|
+
const newStr = str(i.new_string) ?? str(i.new_str) ?? str(i.newText) ?? str(i.newStr)
|
|
123
|
+
return path && oldStr !== undefined && newStr !== undefined ? { name: "str_replace", args: { path, oldStr, newStr } } : undefined
|
|
124
|
+
}
|
|
125
|
+
case "read":
|
|
126
|
+
return path ? { name: "read_file", args: { path } } : undefined
|
|
127
|
+
}
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The chat's icon for a call (the tool_call record's `kind`). */
|
|
132
|
+
export function uiKind(k: ToolKind): string {
|
|
133
|
+
switch (k) {
|
|
134
|
+
case "read":
|
|
135
|
+
case "list":
|
|
136
|
+
return "read"
|
|
137
|
+
case "edit":
|
|
138
|
+
case "write":
|
|
139
|
+
return "edit"
|
|
140
|
+
case "shell":
|
|
141
|
+
return "execute"
|
|
142
|
+
case "search":
|
|
143
|
+
return "search"
|
|
144
|
+
case "web":
|
|
145
|
+
return "fetch"
|
|
146
|
+
}
|
|
147
|
+
return "other"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Applies checkpoint reverts the way Kiro resolves tombstones: a tombstone removes the rows from the one it
|
|
152
|
+
* names up to itself. Rows before a summarization tombstone are kept, since they stay on screen.
|
|
153
|
+
*/
|
|
154
|
+
function revertCheckpoints(rows: KiroRow[]): KiroRow[] {
|
|
155
|
+
const out: KiroRow[] = []
|
|
156
|
+
for (const r of rows) {
|
|
157
|
+
if (r.payload.type !== "tombstone" || r.payload.kind !== "checkpoint_revert") {
|
|
158
|
+
out.push(r)
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
// A tombstone whose target is gone is skipped, as Kiro skips it.
|
|
162
|
+
for (let at = out.length - 1; at >= 0; at--)
|
|
163
|
+
if (out[at]!.id === r.payload.effectiveFromMessageId) {
|
|
164
|
+
out.length = at
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The portable turns of a Kiro session. Everything the chat shows is history, including turns a compaction
|
|
173
|
+
* hid from the model; the compaction itself is its summary row. Returns the id of the session a brain-written
|
|
174
|
+
* session continues, when its note is there.
|
|
175
|
+
*/
|
|
176
|
+
export function turnsOf(rows: KiroRow[], h: Session["header"]): { turns: Turn[]; continued?: string } {
|
|
177
|
+
const turns: Turn[] = []
|
|
178
|
+
const lost = lossCounter()
|
|
179
|
+
let continued: string | undefined
|
|
180
|
+
const calls = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
|
|
181
|
+
const answered = new Set<string>()
|
|
182
|
+
for (const r of revertCheckpoints(rows)) {
|
|
183
|
+
const p = r.payload
|
|
184
|
+
const at = iso(r.timestamp)
|
|
185
|
+
const stamp = at ? { at } : {}
|
|
186
|
+
switch (p.type) {
|
|
187
|
+
case "user": {
|
|
188
|
+
// Prompts Kiro makes up itself (continuations, wake-ups) are not the person's words.
|
|
189
|
+
if (p._meta?.kiro && Object.hasOwn(p._meta.kiro, "syntheticUserMessageReason")) {
|
|
190
|
+
lost.add("Kiro's own follow-up prompts (not carried)")
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(p.images) && p.images.length) lost.add("images", p.images.length)
|
|
194
|
+
if (Array.isArray(p.documents) && p.documents.length) lost.add("documents", p.documents.length)
|
|
195
|
+
const text = String(p.content ?? "")
|
|
196
|
+
if (text.trim()) turns.push({ kind: "user", ...stamp, text })
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
case "assistant": {
|
|
200
|
+
const text = String(p.content ?? "")
|
|
201
|
+
if (p.operationType === "Summary") {
|
|
202
|
+
if (text.trim()) turns.push({ kind: "compaction", ...stamp, summary: text })
|
|
203
|
+
} else if (p.operationType === "Reasoning") {
|
|
204
|
+
if (p.reasoningRedactedContent) lost.add("redacted reasoning")
|
|
205
|
+
else if (text.trim() && text !== ELIDED_REASONING) turns.push({ kind: "reasoning", ...stamp, text })
|
|
206
|
+
} else if (text.trim()) turns.push({ kind: "assistant", ...stamp, text, ...(h.models[0] ? { model: h.models[0] } : {}) })
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
case "tool_call": {
|
|
210
|
+
const id = String(p.toolCallId)
|
|
211
|
+
const args = p.args ?? {}
|
|
212
|
+
const seen = calls.get(id)
|
|
213
|
+
// Kiro writes a call again as its approval and status change; the last arguments are the ones that ran.
|
|
214
|
+
if (seen) seen.input = args
|
|
215
|
+
else {
|
|
216
|
+
const call = { kind: "tool_call" as const, ...stamp, id, name: String(p.toolName), toolKind: kiroToolKind(String(p.toolName)), input: args }
|
|
217
|
+
calls.set(id, call)
|
|
218
|
+
turns.push(call)
|
|
219
|
+
}
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
case "tool_result": {
|
|
223
|
+
const id = String(p.toolCallId)
|
|
224
|
+
if (answered.has(id) || p.metadata?.reverted === true) break
|
|
225
|
+
answered.add(id)
|
|
226
|
+
if (Array.isArray(p.images) && p.images.length) lost.add("images in tool results", p.images.length)
|
|
227
|
+
const c = clip(asText(p.content))
|
|
228
|
+
turns.push({ kind: "tool_result", ...stamp, callId: id, ok: p.success !== false, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
case "sub_agent_start": {
|
|
232
|
+
// The model sees a subagent as an invoke_sub_agent call answered by its final response.
|
|
233
|
+
const id = String(p.subSessionId)
|
|
234
|
+
if (calls.has(id)) break
|
|
235
|
+
const call = { kind: "tool_call" as const, ...stamp, id, name: "invoke_sub_agent", toolKind: "task" as const, input: { name: p.subAgentName, prompt: p.prompt } }
|
|
236
|
+
calls.set(id, call)
|
|
237
|
+
turns.push(call)
|
|
238
|
+
lost.add("subagent transcripts (their results stay in the parent's tool results)")
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
case "sub_agent_complete": {
|
|
242
|
+
const id = String(p.subSessionId)
|
|
243
|
+
if (answered.has(id) || !calls.has(id)) break
|
|
244
|
+
answered.add(id)
|
|
245
|
+
const c = clip(asText(p.response ?? p.errorMessage ?? ""))
|
|
246
|
+
turns.push({ kind: "tool_result", ...stamp, callId: id, ok: p.status === "success", output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
case "agent_note": {
|
|
250
|
+
const note = String(p.content ?? "").match(CONTINUED_NOTE)
|
|
251
|
+
if (note) continued = note[1]
|
|
252
|
+
else lost.add("Kiro agent notes (not carried)")
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
// Turn boundaries, usage, hooks, steering, approvals and session events are not conversation.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lost.flush(h)
|
|
259
|
+
return { turns, ...(continued ? { continued } : {}) }
|
|
260
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { extname, join, resolve } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
6
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
7
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
8
|
+
import { continuedNote } from "../claude-code/records.ts"
|
|
9
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
10
|
+
import { readKiroDir } from "./reader.ts"
|
|
11
|
+
import { type KiroMetadata, type KiroRow, TESTED_VERSIONS, kiroCall, kiroCliVersion, kiroToolKind, sessionsDir, uiKind, workspaceHash } from "./records.ts"
|
|
12
|
+
|
|
13
|
+
// Continues a portable session as a new Kiro V3 session: a folder in ~/.kiro/sessions/<bucket>/ holding the
|
|
14
|
+
// messages.jsonl and session.json that SessionPersistence.saveSession writes, with the records Kiro's own
|
|
15
|
+
// turns produce. Each user turn is a turn_start, the user record, the replies (assistant "Say" text, plain
|
|
16
|
+
// "Reasoning", tool_call and tool_result records paired by toolCallId) and a turn_end. Calls are mapped onto
|
|
17
|
+
// Kiro's built-ins (execute_bash, fs_write, str_replace, read_file) when their arguments carry what those
|
|
18
|
+
// need; others keep their names. A compaction is written as Kiro compacts: a summarization tombstone, which
|
|
19
|
+
// hides the turns before it from the model while the chat still shows them, then the summary row. The note
|
|
20
|
+
// saying where the session came from is an agent_note, which Kiro sends the model and never shows.
|
|
21
|
+
//
|
|
22
|
+
// Kiro writes nothing about a session into a shared database: it lists a workspace from its bucket folder,
|
|
23
|
+
// and its list index re-walks a bucket whose folder changed. So adding a folder needs Kiro neither closed nor
|
|
24
|
+
// told; only the new session's own folder is written. Readable reasoning is kept as Kiro keeps it, shown in
|
|
25
|
+
// the chat; Kiro sends the model only signed reasoning, so it never reaches the model.
|
|
26
|
+
|
|
27
|
+
const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
|
|
28
|
+
const IMAGE_LIMIT = 5 * 1024 * 1024
|
|
29
|
+
const NO_RESULT = "No result was recorded for this call; the session moved before it finished."
|
|
30
|
+
|
|
31
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
32
|
+
|
|
33
|
+
export interface KiroPlan {
|
|
34
|
+
id: string
|
|
35
|
+
bucket: string
|
|
36
|
+
metadata: KiroMetadata
|
|
37
|
+
rows: KiroRow[]
|
|
38
|
+
lost: string[]
|
|
39
|
+
/** What a read-back must find. */
|
|
40
|
+
expect: { users: number; assistants: number; calls: number; compactions: number }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The session folder's contents for `session` continued in `cwd`. Pure apart from reading attached images. */
|
|
44
|
+
export function planKiro(session: Session, opts: { cwd: string; now: Date; id?: string }): KiroPlan {
|
|
45
|
+
const id = opts.id ?? `sess_${randomUUID()}`
|
|
46
|
+
const label = toolLabel(session.header.tool)
|
|
47
|
+
const drops = new Map<string, number>()
|
|
48
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
49
|
+
const turns = session.turns
|
|
50
|
+
const lastCompaction = turns.map((t) => t.kind).lastIndexOf("compaction")
|
|
51
|
+
const expect = { users: 0, assistants: 0, calls: 0, compactions: 0 }
|
|
52
|
+
const results = new Map<string, boolean>()
|
|
53
|
+
for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t.ok)
|
|
54
|
+
|
|
55
|
+
// Tool call ids: Kiro's request builder takes [A-Za-z0-9_-], up to 64; others are mapped once.
|
|
56
|
+
const ids = new Map<string, string>()
|
|
57
|
+
const used = new Set<string>()
|
|
58
|
+
let remapped = 0
|
|
59
|
+
const callId = (raw: string) => {
|
|
60
|
+
let v = ids.get(raw)
|
|
61
|
+
if (v) return v
|
|
62
|
+
const base = /^[A-Za-z0-9_-]{1,64}$/.test(raw) ? raw : `call_${raw.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
|
|
63
|
+
v = base
|
|
64
|
+
for (let n = 2; used.has(v); n++) v = `${base}_${n}`
|
|
65
|
+
ids.set(raw, v)
|
|
66
|
+
used.add(v)
|
|
67
|
+
if (v !== raw) remapped++
|
|
68
|
+
return v
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Record times never go backwards; turns without one take the last one seen.
|
|
72
|
+
const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
|
|
73
|
+
let clock = known ?? opts.now.getTime()
|
|
74
|
+
const time = (at?: string) => {
|
|
75
|
+
const t = at ? Date.parse(at) : Number.NaN
|
|
76
|
+
if (Number.isFinite(t) && t > clock) clock = t
|
|
77
|
+
return new Date(clock).toISOString()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rows: KiroRow[] = []
|
|
81
|
+
const push = (payload: KiroRow["payload"], at?: string, rowId: string = randomUUID()) => {
|
|
82
|
+
rows.push({ id: rowId, timestamp: time(at), payload })
|
|
83
|
+
return rowId
|
|
84
|
+
}
|
|
85
|
+
let turn: { id: string; replied: boolean } | undefined
|
|
86
|
+
const close = (at?: string) => {
|
|
87
|
+
if (!turn) return
|
|
88
|
+
push({ type: "turn_end", stopReason: turn.replied ? "end_turn" : "cancelled", executionId: turn.id, turnId: turn.id, boundarySource: "observed" }, at, `${turn.id}-turn-end`)
|
|
89
|
+
turn = undefined
|
|
90
|
+
}
|
|
91
|
+
const open = (at?: string) => {
|
|
92
|
+
turn = { id: randomUUID(), replied: false }
|
|
93
|
+
push({ type: "turn_start", executionId: turn.id, turnId: turn.id, boundarySource: "observed" }, at, `${turn.id}-turn-start`)
|
|
94
|
+
}
|
|
95
|
+
/** The current turn's identity, opening one for replies that come before any user turn. */
|
|
96
|
+
const inTurn = (at?: string) => {
|
|
97
|
+
if (!turn) open(at)
|
|
98
|
+
turn!.replied = true
|
|
99
|
+
return { executionId: turn!.id, turnId: turn!.id }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
push({ type: "agent_note", content: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)) }, turns[0]?.at, `note_${randomUUID()}`)
|
|
103
|
+
/** The first record after the note: where a compaction's tombstone starts hiding turns from the model. */
|
|
104
|
+
const firstAfterNote = () => rows[1]?.id
|
|
105
|
+
const written = new Set<string>()
|
|
106
|
+
const done = new Set<string>()
|
|
107
|
+
const result = (raw: string, ok: boolean, output: string, at?: string) =>
|
|
108
|
+
push({ type: "tool_result", toolCallId: callId(raw), content: output || "(no output)", success: ok, ...inTurn(at) }, at)
|
|
109
|
+
|
|
110
|
+
for (const [i, t] of turns.entries()) {
|
|
111
|
+
switch (t.kind) {
|
|
112
|
+
case "user": {
|
|
113
|
+
let text = t.text
|
|
114
|
+
const images: Array<{ data: string; mimeType: string }> = []
|
|
115
|
+
for (const a of t.attachments ?? []) {
|
|
116
|
+
if (!a.path) continue // bytes the reader could not keep; it already counted them
|
|
117
|
+
if (a.kind === "file") {
|
|
118
|
+
text += `\n\n[Attached file: ${a.path}]`
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
|
|
122
|
+
let size = Number.POSITIVE_INFINITY
|
|
123
|
+
try {
|
|
124
|
+
size = statSync(a.path).size
|
|
125
|
+
} catch {}
|
|
126
|
+
if (mime && Object.values(IMAGE_TYPES).includes(mime) && size <= IMAGE_LIMIT) images.push({ data: readFileSync(a.path).toString("base64"), mimeType: mime })
|
|
127
|
+
else drop("images that could not be attached")
|
|
128
|
+
}
|
|
129
|
+
if (!text.trim() && !images.length) break
|
|
130
|
+
close(t.at)
|
|
131
|
+
open(t.at)
|
|
132
|
+
push({ type: "user", content: text.trim() ? text : "(image)", source: "chat", ...(images.length ? { images } : {}) }, t.at)
|
|
133
|
+
expect.users++
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
case "assistant":
|
|
137
|
+
if (!t.text.trim()) break
|
|
138
|
+
push({ type: "assistant", content: t.text, operationType: "Say", ...inTurn(t.at) }, t.at)
|
|
139
|
+
expect.assistants++
|
|
140
|
+
break
|
|
141
|
+
case "reasoning":
|
|
142
|
+
if (!t.text.trim() || t.text === "...") break
|
|
143
|
+
push({ type: "assistant", content: t.text, operationType: "Reasoning", ...inTurn(t.at) }, t.at)
|
|
144
|
+
break
|
|
145
|
+
case "tool_call": {
|
|
146
|
+
if (written.has(t.id)) {
|
|
147
|
+
drop("repeated tool call ids")
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
written.add(t.id)
|
|
151
|
+
const mapped = kiroCall(t as Call)
|
|
152
|
+
const name = mapped?.name ?? t.name
|
|
153
|
+
const args = mapped?.args ?? (t.input && typeof t.input === "object" && !Array.isArray(t.input) ? (t.input as Record<string, unknown>) : { input: t.input ?? null })
|
|
154
|
+
const ok = results.get(t.id)
|
|
155
|
+
push(
|
|
156
|
+
{ type: "tool_call", toolCallId: callId(t.id), toolName: name, args, status: ok === false || ok === undefined ? "failed" : "completed", kind: uiKind(mapped ? kiroToolKind(name) : t.toolKind), ...inTurn(t.at) },
|
|
157
|
+
t.at,
|
|
158
|
+
)
|
|
159
|
+
expect.calls++
|
|
160
|
+
if (ok === undefined) {
|
|
161
|
+
result(t.id, false, NO_RESULT, t.at)
|
|
162
|
+
done.add(t.id)
|
|
163
|
+
drop("tool calls without a result (closed as errors)")
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
case "tool_result": {
|
|
168
|
+
if (!written.has(t.callId)) {
|
|
169
|
+
drop("tool results without a call")
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
if (done.has(t.callId)) {
|
|
173
|
+
drop("repeated tool results")
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
done.add(t.callId)
|
|
177
|
+
result(t.callId, t.ok, t.fullPath ? `${t.output}\n\n[Full output: ${t.fullPath}]` : t.output, t.at)
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
case "compaction": {
|
|
181
|
+
close(t.at)
|
|
182
|
+
const summary = `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`
|
|
183
|
+
const from = firstAfterNote()
|
|
184
|
+
// Only the last compaction hides turns: Kiro resolves a tombstone against the rows still in effect.
|
|
185
|
+
if (i === lastCompaction && from)
|
|
186
|
+
push({ type: "tombstone", kind: "summarization", effectiveFromMessageId: from, metadata: { truncatedMessageCount: rows.length - 1, truncatedAt: time(t.at) } }, t.at, `tombstone_summarize_${randomUUID()}`)
|
|
187
|
+
push({ type: "assistant", content: summary, operationType: "Summary" }, t.at, `summary_${randomUUID()}`)
|
|
188
|
+
expect.compactions++
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
case "file_edit":
|
|
192
|
+
if (!t.callId || !written.has(t.callId)) drop("file edits outside a tool call")
|
|
193
|
+
break
|
|
194
|
+
case "subagent":
|
|
195
|
+
drop("subagent transcripts (their results stay in the parent's tool results)")
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
close()
|
|
200
|
+
|
|
201
|
+
const first = rows[0]?.timestamp ?? opts.now.toISOString()
|
|
202
|
+
const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
|
|
203
|
+
const metadata: KiroMetadata = {
|
|
204
|
+
schemaVersion: "1.0.0",
|
|
205
|
+
dataModelVersion: 1,
|
|
206
|
+
id,
|
|
207
|
+
title: `${title} (from ${label})`,
|
|
208
|
+
agentMode: "vibe",
|
|
209
|
+
workspacePaths: [opts.cwd],
|
|
210
|
+
createdAt: first,
|
|
211
|
+
lastModifiedAt: opts.now.toISOString(),
|
|
212
|
+
status: "idle",
|
|
213
|
+
titleSetByUser: true,
|
|
214
|
+
}
|
|
215
|
+
const lost = [...drops].map(([what, n]) => `${what} (${n})`)
|
|
216
|
+
if (remapped) lost.push(`tool call ids, remapped to ids Kiro accepts (${remapped})`)
|
|
217
|
+
return { id, bucket: workspaceHash([opts.cwd]), metadata, rows, lost, expect }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The kiro-cli binary: the installer's ~/.local/bin/kiro-cli first, else whatever is on PATH. */
|
|
221
|
+
function resolveBin(home: string): { path: string; command: string } {
|
|
222
|
+
const own = join(home, ".local", "bin", "kiro-cli")
|
|
223
|
+
if (exists(own)) return { path: own, command: Bun.which("kiro-cli") === own ? "kiro-cli" : own }
|
|
224
|
+
return { path: "kiro-cli", command: "kiro-cli" }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The folder as Kiro CLI reports it to its engine (the process's current directory, so with symlinks
|
|
229
|
+
* resolved: /tmp is /private/tmp on macOS); the session's bucket is keyed by it.
|
|
230
|
+
*/
|
|
231
|
+
export function realCwd(cwd: string): string {
|
|
232
|
+
try {
|
|
233
|
+
return realpathSync(cwd)
|
|
234
|
+
} catch {
|
|
235
|
+
return resolve(cwd)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const sh = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
240
|
+
|
|
241
|
+
export interface KiroTarget {
|
|
242
|
+
tool: string
|
|
243
|
+
label: string
|
|
244
|
+
ready(w: WriteOptions): Promise<Readiness>
|
|
245
|
+
/** How the person opens the new session: a command, and what to do after it. */
|
|
246
|
+
open(w: { home: string; cwd: string; id: string; title: string }): { open: string; note?: string }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A writer into the shared store for one Kiro product. */
|
|
250
|
+
export function kiroWriter(target: KiroTarget): Writer {
|
|
251
|
+
return {
|
|
252
|
+
tool: target.tool,
|
|
253
|
+
label: target.label,
|
|
254
|
+
way: "native",
|
|
255
|
+
ready: (w) => target.ready(w),
|
|
256
|
+
|
|
257
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
258
|
+
const cwd = realCwd(w.cwd)
|
|
259
|
+
const plan = planKiro(session, { cwd, now: new Date() })
|
|
260
|
+
const dir = join(sessionsDir(w.home), plan.bucket, plan.id)
|
|
261
|
+
const files = [join(dir, "messages.jsonl"), join(dir, "session.json")]
|
|
262
|
+
const how = target.open({ home: w.home, cwd, id: plan.id, title: plan.metadata.title })
|
|
263
|
+
const result = { tool: target.tool, way: "native" as const, sessionId: plan.id, written: files, ...how, lost: plan.lost }
|
|
264
|
+
if (w.dryRun) return { ...result, dryRun: true }
|
|
265
|
+
|
|
266
|
+
// Kiro's own modes: the folders private to the person, the files too. The transcript goes first, as
|
|
267
|
+
// Kiro saves it, so a folder with a session.json is always complete.
|
|
268
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
269
|
+
writeFileSync(files[0]!, `${plan.rows.map((r) => JSON.stringify(r)).join("\n")}\n`, { flag: "wx", mode: 0o600 })
|
|
270
|
+
writeFileSync(files[1]!, JSON.stringify(plan.metadata, null, 2), { flag: "wx", mode: 0o600 })
|
|
271
|
+
|
|
272
|
+
// Read it back as the reader reads Kiro; a session that does not round-trip is removed again.
|
|
273
|
+
const back = readKiroDir(dir, w.home, target.tool)
|
|
274
|
+
const n = countTurns(back)
|
|
275
|
+
const pairs = unpaired(back)
|
|
276
|
+
const e = plan.expect
|
|
277
|
+
if (n.user !== e.users || n.assistant !== e.assistants || n.tool_call !== e.calls || n.compaction !== e.compactions || pairs.calls.length || pairs.results.length) {
|
|
278
|
+
rmSync(dir, { recursive: true, force: true })
|
|
279
|
+
throw new Error(
|
|
280
|
+
`the new Kiro session did not read back as written (users ${n.user}/${e.users}, replies ${n.assistant}/${e.assistants}, tool calls ${n.tool_call}/${e.calls}, summaries ${n.compaction}/${e.compactions}, unpaired ${pairs.calls.length + pairs.results.length}); it was removed`,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
return { ...result, dryRun: false }
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Kiro CLI: its V3 engine (the default since 3.0) resumes the session by id from the folder it ran in. */
|
|
289
|
+
export function kiroCliWriter(opts: { bin?: string; version?: string } = {}): Writer {
|
|
290
|
+
const binFor = (home: string) => (opts.bin ? { path: opts.bin, command: opts.bin } : resolveBin(home))
|
|
291
|
+
return kiroWriter({
|
|
292
|
+
tool: "kiro-cli",
|
|
293
|
+
label: "Kiro CLI",
|
|
294
|
+
async ready(w) {
|
|
295
|
+
const v = opts.version ?? kiroCliVersion(binFor(w.home).path)
|
|
296
|
+
if (!v) return { ok: false, why: "Kiro CLI is not installed (`kiro-cli --version` did not run)" }
|
|
297
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Kiro CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
298
|
+
if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
|
|
299
|
+
return { ok: true }
|
|
300
|
+
},
|
|
301
|
+
open: ({ home, cwd, id }) => ({ open: `cd ${sh(cwd)} && ${sh(binFor(home).command)} chat --resume-id ${id}` }),
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const kiroCliNativeWriter = kiroCliWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { kiroIdeNativeWriter } from "./writer.ts"
|
|
3
|
+
|
|
4
|
+
// Kiro IDE: continues any session as a new chat in Kiro's V3 session store. The IDE shares that store with
|
|
5
|
+
// Kiro CLI, whose reader (kiro-cli) lists and reads both products' sessions; a second reader here would list
|
|
6
|
+
// every session twice, since a session does not record which product made it.
|
|
7
|
+
export const readers: Reader[] = []
|
|
8
|
+
export const writers: Writer[] = [kiroIdeNativeWriter]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
3
|
+
import type { Writer } from "../types.ts"
|
|
4
|
+
import { kiroWriter, sh } from "../kiro-cli/writer.ts"
|
|
5
|
+
import { ENGINE } from "../kiro-cli/records.ts"
|
|
6
|
+
|
|
7
|
+
// Continues a portable session in Kiro IDE. Since 1.0 the IDE keeps its chats in the same V3 store as Kiro
|
|
8
|
+
// CLI (~/.kiro/sessions/<bucket>/<id>/, the bucket keyed by the workspace's folders), so the session is
|
|
9
|
+
// written exactly as for the CLI and appears in the IDE's chat history for that folder.
|
|
10
|
+
//
|
|
11
|
+
// The shape was checked against the agent engine Kiro CLI 3.1.0 ships (see ENGINE), not against an IDE: no
|
|
12
|
+
// Kiro IDE was available to check which engine it bundles. So no IDE version is listed as tested yet, and
|
|
13
|
+
// ready() says so; add a version here once a session written by this writer has been opened in it.
|
|
14
|
+
|
|
15
|
+
export const TESTED_IDE_VERSIONS: string[] = []
|
|
16
|
+
|
|
17
|
+
/** Where Kiro IDE is installed, if it is: the system Applications folder or the person's own. */
|
|
18
|
+
export function kiroApp(home: string): string | undefined {
|
|
19
|
+
return ["/Applications/Kiro.app", join(home, "Applications", "Kiro.app")].find((p) => isDir(p))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The app's CFBundleShortVersionString, e.g. "1.4.121". */
|
|
23
|
+
export function appVersion(app: string): string | undefined {
|
|
24
|
+
const plist = join(app, "Contents", "Info.plist")
|
|
25
|
+
if (!exists(plist)) return undefined
|
|
26
|
+
const p = Bun.spawnSync(["plutil", "-extract", "CFBundleShortVersionString", "raw", plist], { stdout: "pipe", stderr: "pipe" })
|
|
27
|
+
return p.exitCode === 0 ? p.stdout.toString().trim() || undefined : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The Kiro IDE writer; `app` and `version` are for tests, which must not depend on an installed Kiro. */
|
|
31
|
+
export function kiroIdeWriter(opts: { app?: string; version?: string; tested?: string[] } = {}): Writer {
|
|
32
|
+
const tested = opts.tested ?? TESTED_IDE_VERSIONS
|
|
33
|
+
return kiroWriter({
|
|
34
|
+
tool: "kiro-ide",
|
|
35
|
+
label: "Kiro IDE",
|
|
36
|
+
async ready(w) {
|
|
37
|
+
const app = opts.app ?? kiroApp(w.home)
|
|
38
|
+
if (!app || !isDir(app)) return { ok: false, why: "Kiro IDE is not installed (no Kiro.app in /Applications or ~/Applications)" }
|
|
39
|
+
const v = opts.version ?? appVersion(app)
|
|
40
|
+
if (!v || !tested.includes(v))
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
why: `Kiro IDE ${v ?? "(unknown version)"} is not a version this writer was checked against (${tested.join(", ") || `none yet; the format was checked against ${ENGINE} in Kiro CLI 3.1.0`})`,
|
|
44
|
+
}
|
|
45
|
+
if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
|
|
46
|
+
return { ok: true }
|
|
47
|
+
},
|
|
48
|
+
open: ({ cwd, title }) => ({ open: `open -a Kiro ${sh(cwd)}`, note: `In Kiro's chat, open the session history and pick "${title}".` }),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const kiroIdeNativeWriter = kiroIdeWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { lettaReader } from "./reader.ts"
|
|
3
|
+
import { lettaNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Letta Code: reads and writes conversations of the local backend's agents (~/.letta/lc-local-backend).
|
|
6
|
+
// Server ("cloud") agents keep their history on the Letta server and are not on disk.
|
|
7
|
+
export const readers: Reader[] = [lettaReader]
|
|
8
|
+
export const writers: Writer[] = [lettaNativeWriter]
|