@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,220 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { exists, isDir, versionOf } from "../../fsutil.ts"
|
|
4
|
+
|
|
5
|
+
// What Aider's chat history looks like, as far as this adapter relies on it. Checked against aider-chat
|
|
6
|
+
// 0.86.2 (PyPI) and its source at Aider-AI/aider 5dc9490 (aider/io.py, aider/utils.py,
|
|
7
|
+
// aider/coders/base_coder.py, aider/args.py, aider/history.py). Catalogue entry 13.
|
|
8
|
+
//
|
|
9
|
+
// Aider keeps one Markdown file per project, `.aider.chat.history.md` at the git root (else the folder it
|
|
10
|
+
// runs in), and appends to it for ever: each launch writes "# aider chat started at <local time>", the
|
|
11
|
+
// person's input as "#### " lines, Aider's own notices as "> " lines and the model's replies as plain
|
|
12
|
+
// text. `aider --restore-chat-history` reads the whole file back with utils.split_chat_history_markdown:
|
|
13
|
+
// "# " lines are skipped, "> " lines are dropped (they are its own output, never sent to the model), runs
|
|
14
|
+
// of "#### " lines are one user message, and everything else is the assistant's. There are no ids, no tool
|
|
15
|
+
// calls and no reasoning.
|
|
16
|
+
|
|
17
|
+
export const TOOL = "aider"
|
|
18
|
+
export const LABEL = "Aider"
|
|
19
|
+
|
|
20
|
+
/** Aider versions whose history format the writer was checked against. */
|
|
21
|
+
export const TESTED_VERSIONS = ["0.86.2"]
|
|
22
|
+
|
|
23
|
+
export const HISTORY_FILE = ".aider.chat.history.md"
|
|
24
|
+
export const INPUT_HISTORY_FILE = ".aider.input.history"
|
|
25
|
+
|
|
26
|
+
/** The line each Aider launch starts with (io.py: datetime.now().strftime("%Y-%m-%d %H:%M:%S")). */
|
|
27
|
+
export const RUN_HEADER = /^# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s*$/
|
|
28
|
+
|
|
29
|
+
/** What Aider's own summariser puts before a summary that replaced earlier messages (prompts.summary_prefix). */
|
|
30
|
+
export const SUMMARY_PREFIX = "I spoke to you previously about a number of things.\n"
|
|
31
|
+
|
|
32
|
+
/** The note a continued session opens with; the reader recognises it and records where the session came from. */
|
|
33
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
34
|
+
`[This session continues ${label} session ${id}, carried over by brain session on ${when}. The messages below are its history; tool calls are shown as text, and reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.]`
|
|
35
|
+
export const CONTINUED_NOTE = /^\[This session continues .+? session (\S+), carried over by brain session/
|
|
36
|
+
|
|
37
|
+
/** The installed Aider version ("aider 0.86.2" → "0.86.2"), or undefined when it is missing. */
|
|
38
|
+
export const aiderVersion = (bin = "aider") => versionOf(bin)?.match(/(\d+\.\d+\.\d+(?:\.dev\d*)?)/)?.[1]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The folder Aider keeps its history in for `cwd`: the enclosing git working tree (args.py defaults the
|
|
42
|
+
* history files to the git root; main.get_git_root searches parent folders), else `cwd` itself.
|
|
43
|
+
*/
|
|
44
|
+
export function historyRoot(cwd: string): string {
|
|
45
|
+
let dir = cwd.replace(/\/+$/, "") || "/"
|
|
46
|
+
for (let i = 0; i < 40; i++) {
|
|
47
|
+
if (exists(join(dir, ".git"))) return dir
|
|
48
|
+
const up = dir.slice(0, dir.lastIndexOf("/")) || "/"
|
|
49
|
+
if (up === dir) break
|
|
50
|
+
dir = up
|
|
51
|
+
}
|
|
52
|
+
return cwd
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Line breaks Python's str.splitlines() splits on; the parser sees each as a new line. */
|
|
56
|
+
const PY_LINE_BREAKS = /\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/
|
|
57
|
+
|
|
58
|
+
/** Text split into lines the way split_chat_history_markdown splits it, without the line endings. */
|
|
59
|
+
export const pyLines = (text: string) => text.split(PY_LINE_BREAKS)
|
|
60
|
+
|
|
61
|
+
/** One message as Aider's parser yields it. */
|
|
62
|
+
export interface MdMessage {
|
|
63
|
+
role: "user" | "assistant" | "tool"
|
|
64
|
+
content: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One launch of Aider: its header time and its messages, tool lines included. */
|
|
68
|
+
export interface Run {
|
|
69
|
+
startedAt?: string
|
|
70
|
+
/** 0-based index of the run in the file. */
|
|
71
|
+
index: number
|
|
72
|
+
messages: MdMessage[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The file split into runs, each parsed exactly as utils.split_chat_history_markdown parses (with
|
|
77
|
+
* include_tool=True, so the reader can see Aider's notices; the model never gets the "tool" ones).
|
|
78
|
+
*/
|
|
79
|
+
export function parseRuns(text: string): Run[] {
|
|
80
|
+
const runs: Run[] = []
|
|
81
|
+
let run: Run = { index: 0, messages: [] }
|
|
82
|
+
let user: string[] = []
|
|
83
|
+
let assistant: string[] = []
|
|
84
|
+
let tool: string[] = []
|
|
85
|
+
const append = (role: MdMessage["role"], lines: string[]) => {
|
|
86
|
+
const content = lines.join("")
|
|
87
|
+
if (content.trim()) run.messages.push({ role, content })
|
|
88
|
+
}
|
|
89
|
+
const flushAll = () => {
|
|
90
|
+
append("assistant", assistant)
|
|
91
|
+
append("user", user)
|
|
92
|
+
append("tool", tool)
|
|
93
|
+
assistant = []
|
|
94
|
+
user = []
|
|
95
|
+
tool = []
|
|
96
|
+
}
|
|
97
|
+
const parts = text.split(/(\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029])/)
|
|
98
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
99
|
+
const body = parts[i]!
|
|
100
|
+
const line = body + (parts[i + 1] ?? "")
|
|
101
|
+
if (!line) continue
|
|
102
|
+
const header = body.match(RUN_HEADER)
|
|
103
|
+
if (header) {
|
|
104
|
+
// A new launch: Aider's parser would carry on across it, but the reader keeps each launch apart.
|
|
105
|
+
flushAll()
|
|
106
|
+
if (run.messages.length || run.startedAt) runs.push(run)
|
|
107
|
+
run = { index: runs.length, startedAt: header[1]!, messages: [] }
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if (line.startsWith("# ")) continue
|
|
111
|
+
if (line.startsWith("> ")) {
|
|
112
|
+
append("assistant", assistant)
|
|
113
|
+
assistant = []
|
|
114
|
+
append("user", user)
|
|
115
|
+
user = []
|
|
116
|
+
tool.push(line.slice(2))
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (line.startsWith("#### ")) {
|
|
120
|
+
append("assistant", assistant)
|
|
121
|
+
assistant = []
|
|
122
|
+
append("tool", tool)
|
|
123
|
+
tool = []
|
|
124
|
+
user.push(line.slice(5))
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
append("user", user)
|
|
128
|
+
user = []
|
|
129
|
+
append("tool", tool)
|
|
130
|
+
tool = []
|
|
131
|
+
assistant.push(line)
|
|
132
|
+
}
|
|
133
|
+
flushAll()
|
|
134
|
+
if (run.messages.length || run.startedAt) runs.push(run)
|
|
135
|
+
return runs
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A user message as the person typed it: Aider ends each "#### " line with a Markdown line break (" "). */
|
|
139
|
+
export const userText = (content: string) =>
|
|
140
|
+
pyLines(content)
|
|
141
|
+
.map((l) => l.replace(/ {2}$/, ""))
|
|
142
|
+
.join("\n")
|
|
143
|
+
.replace(/^\n+|\s+$/g, "")
|
|
144
|
+
.replace(/^<blank>$/, "")
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Lines the parser would take for something else get one more leading space from the writer, and lose one
|
|
148
|
+
* in the reader; lines that already start with spaces before such a marker get one too, so the round trip
|
|
149
|
+
* is exact.
|
|
150
|
+
*/
|
|
151
|
+
const CLASHES = /^ *(# |#### |> )/
|
|
152
|
+
const ESCAPED = /^ +(# |#### |> )/
|
|
153
|
+
export const escapeLine = (line: string) => (CLASHES.test(line) ? ` ${line}` : line)
|
|
154
|
+
export const unescapeLine = (line: string) => (ESCAPED.test(line) ? line.slice(1) : line)
|
|
155
|
+
|
|
156
|
+
/** An assistant message as written: the escape spaces taken out, surrounding blank lines trimmed. */
|
|
157
|
+
export const assistantText = (content: string) => pyLines(content).map(unescapeLine).join("\n").trim()
|
|
158
|
+
|
|
159
|
+
/** Folders the history search never enters: system and package folders that hold no projects. */
|
|
160
|
+
const SKIP_DIRS = new Set(["node_modules", "Library", "Applications", "Pictures", "Music", "Movies", "venv", "__pycache__", "dist", "build", "target", "vendor"])
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Every Aider history file in the folders under `home`, at most `depth` levels down. Aider keeps no index
|
|
164
|
+
* of its projects, so they are found by name; hidden and package folders are skipped and the walk stops
|
|
165
|
+
* after `budget` folders so listing stays cheap.
|
|
166
|
+
*/
|
|
167
|
+
export function findHistories(home: string, depth = 5, budget = 20_000): string[] {
|
|
168
|
+
const cache = join(home, ".brain", "cache", "aider-histories.json")
|
|
169
|
+
const fresh = process.env.BRAIN_NO_CACHE !== "1" ? readCache(cache) : undefined
|
|
170
|
+
const found = new Set(fresh ?? walkHistories(home, depth, budget))
|
|
171
|
+
if (!fresh && process.env.BRAIN_NO_CACHE !== "1") writeCache(cache, [...found])
|
|
172
|
+
// The folder the person is in, and the folders above it, are always checked: a project started since the cache was made is found.
|
|
173
|
+
for (let dir = process.cwd(), i = 0; i < depth && dir.startsWith(home); dir = dirname(dir), i++) {
|
|
174
|
+
const p = join(dir, HISTORY_FILE)
|
|
175
|
+
if (exists(p)) found.add(p)
|
|
176
|
+
}
|
|
177
|
+
return [...found]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** How long a walk of the home folder is trusted. Aider keeps no index, so the walk is the only way to find its projects. */
|
|
181
|
+
const CACHE_TTL_MS = 10 * 60_000
|
|
182
|
+
|
|
183
|
+
function readCache(file: string): string[] | undefined {
|
|
184
|
+
try {
|
|
185
|
+
if (Date.now() - statSync(file).mtimeMs > CACHE_TTL_MS) return undefined
|
|
186
|
+
const j = JSON.parse(readFileSync(file, "utf8"))
|
|
187
|
+
return Array.isArray(j?.paths) ? j.paths.filter((p: unknown): p is string => typeof p === "string" && exists(p)) : undefined
|
|
188
|
+
} catch {
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function writeCache(file: string, paths: string[]) {
|
|
194
|
+
try {
|
|
195
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
196
|
+
writeFileSync(file, JSON.stringify({ paths }))
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function walkHistories(home: string, depth: number, budget: number): string[] {
|
|
201
|
+
const out: string[] = []
|
|
202
|
+
let left = budget
|
|
203
|
+
const walk = (dir: string, level: number) => {
|
|
204
|
+
if (left-- <= 0) return
|
|
205
|
+
let entries: import("node:fs").Dirent[]
|
|
206
|
+
try {
|
|
207
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
208
|
+
} catch {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
for (const e of entries) if (e.name === HISTORY_FILE && (e.isFile() || e.isSymbolicLink())) out.push(join(dir, e.name))
|
|
212
|
+
if (level >= depth) return
|
|
213
|
+
for (const e of entries) {
|
|
214
|
+
if (!e.isDirectory() || e.name.startsWith(".") || SKIP_DIRS.has(e.name) || e.name.endsWith(".app")) continue
|
|
215
|
+
walk(join(dir, e.name), level + 1)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (isDir(home)) walk(home, 0)
|
|
219
|
+
return out
|
|
220
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeSync } from "node:fs"
|
|
2
|
+
import { join, resolve } from "node:path"
|
|
3
|
+
import { toolLabel } from "../../brief.ts"
|
|
4
|
+
import { exists, isDir } from "../../fsutil.ts"
|
|
5
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
6
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
7
|
+
import { runId } from "./reader.ts"
|
|
8
|
+
import {
|
|
9
|
+
HISTORY_FILE,
|
|
10
|
+
INPUT_HISTORY_FILE,
|
|
11
|
+
LABEL,
|
|
12
|
+
SUMMARY_PREFIX,
|
|
13
|
+
TESTED_VERSIONS,
|
|
14
|
+
TOOL,
|
|
15
|
+
aiderVersion,
|
|
16
|
+
assistantText,
|
|
17
|
+
continuedNote,
|
|
18
|
+
escapeLine,
|
|
19
|
+
historyRoot,
|
|
20
|
+
parseRuns,
|
|
21
|
+
pyLines,
|
|
22
|
+
userText,
|
|
23
|
+
} from "./records.ts"
|
|
24
|
+
|
|
25
|
+
// Continues a portable session in Aider as a new run appended to the project's chat history
|
|
26
|
+
// (.aider.chat.history.md at the git root, else the folder), written the way Aider's InputOutput writes
|
|
27
|
+
// one: the "# aider chat started at" line, each request as "#### " lines ending in a Markdown line break,
|
|
28
|
+
// each reply as plain text between blank lines. `aider --restore-chat-history` then gives the model the
|
|
29
|
+
// whole file as messages (utils.split_chat_history_markdown), this run included.
|
|
30
|
+
//
|
|
31
|
+
// Why append, not a fresh file with the old one backed up: Aider treats the file as the project's one
|
|
32
|
+
// history. Every launch appends to it and --restore-chat-history replays all of it, so a continued session
|
|
33
|
+
// is one more launch in that history; moving the existing file aside would hide the person's earlier Aider
|
|
34
|
+
// work from Aider, which is modifying another session. Appending leaves every earlier byte as it was, and a
|
|
35
|
+
// run that does not read back is cut off again at the length the file had before.
|
|
36
|
+
//
|
|
37
|
+
// Aider has no record for tool calls or reasoning. Tool calls and their results travel as text inside the
|
|
38
|
+
// reply they belong to (that is what the model would see in an Aider chat anyway: edits and commands live in
|
|
39
|
+
// the reply's text), reasoning is dropped. Lines the parser would misread ("# ", "#### ", "> " at the start
|
|
40
|
+
// of a reply line) get one leading space, which Markdown ignores. A compaction is written the way Aider's
|
|
41
|
+
// own summariser hands one to the model: a user message starting "I spoke to you previously about a number
|
|
42
|
+
// of things." followed by the turns after it. The prompts also go into .aider.input.history, so the arrow
|
|
43
|
+
// keys recall them as they would in Aider.
|
|
44
|
+
|
|
45
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
46
|
+
|
|
47
|
+
const pad = (n: number, w = 2) => String(n).padStart(w, "0")
|
|
48
|
+
/** Local time as Python's datetime.now().strftime("%Y-%m-%d %H:%M:%S"). */
|
|
49
|
+
const localStamp = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
50
|
+
/** Local time as str(datetime.now()), which prompt_toolkit's FileHistory writes before each entry. */
|
|
51
|
+
const pyNow = (d: Date) => `${localStamp(d)}.${pad(d.getMilliseconds(), 3)}000`
|
|
52
|
+
|
|
53
|
+
/** A fence longer than any run of backticks in the text, so the text cannot close it. */
|
|
54
|
+
const fence = (text: string) => "`".repeat(Math.max(3, ...[...text.matchAll(/`+/g)].map((m) => m[0].length + 1)))
|
|
55
|
+
const fenced = (text: string, lang = "") => {
|
|
56
|
+
const f = fence(text)
|
|
57
|
+
return `${f}${lang}\n${text.replace(/\n+$/, "")}\n${f}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Text with every line break Aider's parser knows as "\n", so what is written is what reads back. */
|
|
61
|
+
const oneBreak = (text: string) => pyLines(text).join("\n")
|
|
62
|
+
|
|
63
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
64
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
65
|
+
|
|
66
|
+
/** A tool call and its result as reply text. */
|
|
67
|
+
function callText(call: Call, result: Result | undefined): string {
|
|
68
|
+
const input = typeof call.input === "string" ? call.input : JSON.stringify(call.input ?? {}, null, 2)
|
|
69
|
+
const parts = [`[Tool call: ${call.name} (${call.toolKind})]`, fenced(input, typeof call.input === "string" ? "" : "json")]
|
|
70
|
+
if (!result) parts.push("[No result was recorded for this call.]")
|
|
71
|
+
else {
|
|
72
|
+
let out = result.output || "(no output)"
|
|
73
|
+
if (result.fullPath) out += `\n\n[Full output: ${result.fullPath}]`
|
|
74
|
+
parts.push(result.ok ? "[Result]" : "[Result: failed]", fenced(out))
|
|
75
|
+
}
|
|
76
|
+
return parts.join("\n")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MdPlan {
|
|
80
|
+
/** The run's text, appended to the history file as it stands. */
|
|
81
|
+
history: string
|
|
82
|
+
/** The prompts, in prompt_toolkit FileHistory form, appended to the input history. */
|
|
83
|
+
input: string
|
|
84
|
+
/** The messages Aider's parser must find in the run, for the read-back. */
|
|
85
|
+
messages: Array<{ role: "user" | "assistant"; text: string }>
|
|
86
|
+
lost: string[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The run that continues `session` in Aider. Pure. */
|
|
90
|
+
export function planRun(session: Session, now: Date): MdPlan {
|
|
91
|
+
const drops = new Map<string, number>()
|
|
92
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
93
|
+
const label = toolLabel(session.header.tool)
|
|
94
|
+
|
|
95
|
+
// From the last compaction on: Aider keeps no compaction in its file, it hands the model the summary instead.
|
|
96
|
+
let start = 0
|
|
97
|
+
let summary: string | undefined
|
|
98
|
+
session.turns.forEach((t, i) => {
|
|
99
|
+
if (t.kind === "compaction") {
|
|
100
|
+
start = i + 1
|
|
101
|
+
summary = t.summary
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
const before = session.turns.slice(0, start).filter((t) => t.kind !== "compaction").length
|
|
105
|
+
if (before) drop("turns before the last compaction, replaced by its summary", before)
|
|
106
|
+
|
|
107
|
+
const results = new Map<string, Result>()
|
|
108
|
+
for (const t of session.turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
109
|
+
const callIds = new Set(session.turns.filter((t) => t.kind === "tool_call").map((t) => (t as Call).id))
|
|
110
|
+
|
|
111
|
+
const messages: MdPlan["messages"] = []
|
|
112
|
+
const prompts: string[] = []
|
|
113
|
+
let reply: string[] = []
|
|
114
|
+
const flush = () => {
|
|
115
|
+
const text = oneBreak(reply.join("\n\n")).trim()
|
|
116
|
+
if (text) messages.push({ role: "assistant", text })
|
|
117
|
+
reply = []
|
|
118
|
+
}
|
|
119
|
+
const user = (text: string, prompt: boolean) => {
|
|
120
|
+
flush()
|
|
121
|
+
messages.push({ role: "user", text: oneBreak(text) })
|
|
122
|
+
if (prompt) prompts.push(oneBreak(text))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
user(continuedNote(label, session.header.id, now.toISOString().slice(0, 10)), false)
|
|
126
|
+
if (summary) user(`${SUMMARY_PREFIX}${summary}`, false)
|
|
127
|
+
const seen = new Set<string>()
|
|
128
|
+
for (const t of session.turns.slice(start)) {
|
|
129
|
+
switch (t.kind) {
|
|
130
|
+
case "user":
|
|
131
|
+
if (t.attachments?.length) drop("attachments (Aider's history holds text only)", t.attachments.length)
|
|
132
|
+
if (t.text.trim()) user(t.text, true)
|
|
133
|
+
break
|
|
134
|
+
case "assistant":
|
|
135
|
+
if (t.text.trim()) reply.push(t.text.trim())
|
|
136
|
+
break
|
|
137
|
+
case "tool_call":
|
|
138
|
+
if (seen.has(t.id)) break
|
|
139
|
+
seen.add(t.id)
|
|
140
|
+
reply.push(callText(t, results.get(t.id)))
|
|
141
|
+
drop("tool calls, kept as text in the replies (Aider has no tool-call record)")
|
|
142
|
+
if (!results.has(t.id)) drop("tool calls without a result")
|
|
143
|
+
break
|
|
144
|
+
case "tool_result":
|
|
145
|
+
if (!callIds.has(t.callId)) drop("tool results without a call")
|
|
146
|
+
break
|
|
147
|
+
case "file_edit":
|
|
148
|
+
if (t.callId && callIds.has(t.callId)) break
|
|
149
|
+
reply.push([`[Edited ${t.path}]`, ...(t.patch ? [fenced(t.patch, "diff")] : [])].join("\n"))
|
|
150
|
+
drop("file edits outside a tool call, kept as text")
|
|
151
|
+
break
|
|
152
|
+
case "reasoning":
|
|
153
|
+
drop("reasoning (Aider's history has no place for it)")
|
|
154
|
+
break
|
|
155
|
+
case "subagent":
|
|
156
|
+
drop("subagent sessions")
|
|
157
|
+
break
|
|
158
|
+
case "compaction":
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
flush()
|
|
163
|
+
|
|
164
|
+
// As InputOutput writes them: user_input() and ai_output() through append_chat_history().
|
|
165
|
+
const lines = [`\n# aider chat started at ${localStamp(now)}\n\n`]
|
|
166
|
+
for (const m of messages) {
|
|
167
|
+
if (m.role === "user") {
|
|
168
|
+
const body = pyLines(m.text).join(" \n#### ").replace(/\s+$/, "")
|
|
169
|
+
lines.push(`\n#### ${body} \n`)
|
|
170
|
+
} else {
|
|
171
|
+
lines.push(`\n${pyLines(m.text).map(escapeLine).join("\n")}\n\n`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const input = prompts.map((p) => `\n# ${pyNow(now)}\n${p.split("\n").map((l) => `+${l}\n`).join("")}`).join("")
|
|
175
|
+
return { history: lines.join(""), input, messages, lost: [...drops].map(([what, n]) => `${what} (${n})`) }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Appends `text` to `path` and returns how long the file was before, so a failed run can be cut off again. */
|
|
179
|
+
function append(path: string, text: string): number {
|
|
180
|
+
const size = exists(path) ? statSync(path).size : -1
|
|
181
|
+
const fd = openSync(path, "a")
|
|
182
|
+
try {
|
|
183
|
+
writeSync(fd, text)
|
|
184
|
+
} finally {
|
|
185
|
+
closeSync(fd)
|
|
186
|
+
}
|
|
187
|
+
return size
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Puts a file back to the length it had (or removes it when it did not exist). */
|
|
191
|
+
function restore(path: string, size: number) {
|
|
192
|
+
if (size < 0) return rmSync(path, { force: true })
|
|
193
|
+
const fd = openSync(path, "r+")
|
|
194
|
+
try {
|
|
195
|
+
ftruncateSync(fd, size)
|
|
196
|
+
} finally {
|
|
197
|
+
closeSync(fd)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Whether an Aider is running: its command line runs the `aider` script (a pip install runs it through Python,
|
|
203
|
+
* so the process name is not "aider"). A running Aider appends its own lines to the history it has open, and
|
|
204
|
+
* lines it wrote after the continued run would read as part of it.
|
|
205
|
+
*/
|
|
206
|
+
function aiderRunning(): boolean {
|
|
207
|
+
try {
|
|
208
|
+
return Bun.spawnSync(["pgrep", "-f", "(^|/)aider( |$)"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0
|
|
209
|
+
} catch {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The Aider writer; `version` and `running` are for tests, which must not depend on an installed Aider. */
|
|
215
|
+
export function aiderWriter(opts: { version?: string; running?: () => boolean } = {}): Writer {
|
|
216
|
+
const installed = () => opts.version ?? aiderVersion()
|
|
217
|
+
const running = opts.running ?? aiderRunning
|
|
218
|
+
return {
|
|
219
|
+
tool: TOOL,
|
|
220
|
+
label: LABEL,
|
|
221
|
+
way: "native",
|
|
222
|
+
|
|
223
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
224
|
+
const v = installed()
|
|
225
|
+
if (!v) return { ok: false, why: "Aider is not installed (`aider --version` did not run)" }
|
|
226
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Aider ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
227
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Aider keeps its history in the project` }
|
|
228
|
+
if (running()) return { ok: false, why: "Aider is running; quit it first, or its next lines would land inside the continued session's history" }
|
|
229
|
+
return { ok: true }
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
233
|
+
const cwd = resolve(w.cwd)
|
|
234
|
+
const root = historyRoot(cwd)
|
|
235
|
+
const historyPath = join(root, HISTORY_FILE)
|
|
236
|
+
const inputPath = join(root, INPUT_HISTORY_FILE)
|
|
237
|
+
const plan = planRun(session, new Date())
|
|
238
|
+
let sessionId: string | undefined
|
|
239
|
+
if (!w.dryRun) {
|
|
240
|
+
mkdirSync(root, { recursive: true })
|
|
241
|
+
const historySize = append(historyPath, plan.history)
|
|
242
|
+
// Read back as Aider will: the last run must hold exactly the messages planned.
|
|
243
|
+
const runs = parseRuns(readFileSync(historyPath, "utf8"))
|
|
244
|
+
const got = (runs[runs.length - 1]?.messages ?? []).filter((m) => m.role !== "tool")
|
|
245
|
+
const same =
|
|
246
|
+
got.length === plan.messages.length &&
|
|
247
|
+
got.every((m, i) => m.role === plan.messages[i]!.role && (m.role === "user" ? userText(m.content) : assistantText(m.content)).trim() === plan.messages[i]!.text.trim())
|
|
248
|
+
if (!same) {
|
|
249
|
+
restore(historyPath, historySize)
|
|
250
|
+
throw new Error(`the Aider history did not read back as written (${got.length} of ${plan.messages.length} messages); the file was put back as it was`)
|
|
251
|
+
}
|
|
252
|
+
if (plan.input) append(inputPath, plan.input)
|
|
253
|
+
const run = runs[runs.length - 1]!
|
|
254
|
+
sessionId = runId(historyPath, run, runs)
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
tool: TOOL,
|
|
258
|
+
way: "native",
|
|
259
|
+
...(sessionId ? { sessionId } : {}),
|
|
260
|
+
written: plan.input ? [historyPath, inputPath] : [historyPath],
|
|
261
|
+
open: `cd ${shellQuote(cwd)} && aider --restore-chat-history`,
|
|
262
|
+
lost: plan.lost,
|
|
263
|
+
dryRun: !!w.dryRun,
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const aiderNativeWriter = aiderWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { amazonQCliReader } from "./reader.ts"
|
|
3
|
+
import { amazonQCliNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Amazon Q Developer CLI (`q chat`, superseded by Kiro CLI): reads the per-folder conversations in
|
|
6
|
+
// data.sqlite3 and continues any session as the folder's conversation, which `q chat --resume` restores.
|
|
7
|
+
export const readers: Reader[] = [amazonQCliReader]
|
|
8
|
+
export const writers: Writer[] = [amazonQCliNativeWriter]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { exists, gitInfo } from "../../fsutil.ts"
|
|
3
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
4
|
+
import type { Session } from "../../session/types.ts"
|
|
5
|
+
import { header, iso } from "../../session/util.ts"
|
|
6
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
7
|
+
import { type ConversationState, LABEL, TOOL, dbPath, turnsOf } from "./records.ts"
|
|
8
|
+
|
|
9
|
+
// Amazon Q CLI: one conversation per folder in data.sqlite3's `conversations` table, read from a copy with its
|
|
10
|
+
// write-ahead log. Only conversations with history are listed: those are the ones `q chat --resume` restores.
|
|
11
|
+
|
|
12
|
+
const firstPrompt = (s: ConversationState) => {
|
|
13
|
+
for (const e of s.history ?? []) {
|
|
14
|
+
const c = e.user?.content as Record<string, any> | undefined
|
|
15
|
+
const p = c?.Prompt?.prompt ?? c?.CancelledToolUses?.prompt
|
|
16
|
+
if (typeof p === "string" && p.trim()) return p.replace(/\s+/g, " ").trim().slice(0, 80)
|
|
17
|
+
}
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lastTime = (s: ConversationState) => {
|
|
22
|
+
for (let i = (s.history?.length ?? 0) - 1; i >= 0; i--) {
|
|
23
|
+
const t = iso(s.history[i]!.user?.timestamp)
|
|
24
|
+
if (t) return t
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rows(d: Database): Array<{ key: string; state: ConversationState }> {
|
|
30
|
+
const out: Array<{ key: string; state: ConversationState }> = []
|
|
31
|
+
for (const r of d.query("SELECT key, value FROM conversations").all() as Array<{ key: string; value: string }>) {
|
|
32
|
+
try {
|
|
33
|
+
const state = JSON.parse(r.value) as ConversationState
|
|
34
|
+
if (Array.isArray(state.history) && state.history.length) out.push({ key: r.key, state })
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The conversation stored for `key`, as a portable session. */
|
|
41
|
+
export function readQ(d: Database, path: string, key: string, home: string): Session {
|
|
42
|
+
const r = d.query("SELECT value FROM conversations WHERE key = ?").get(key) as { value: string } | null
|
|
43
|
+
if (!r) throw new Error(`no Amazon Q conversation for ${key} in ${path}`)
|
|
44
|
+
const state = JSON.parse(r.value) as ConversationState
|
|
45
|
+
const title = firstPrompt(state)
|
|
46
|
+
const h = header(TOOL, String(state.conversation_id), {
|
|
47
|
+
sourcePath: path,
|
|
48
|
+
...(title ? { title } : {}),
|
|
49
|
+
project: { cwd: key, ...gitInfo(key, home) },
|
|
50
|
+
})
|
|
51
|
+
const { turns, continued } = turnsOf(state, h)
|
|
52
|
+
if (continued) h.parent = { id: continued, kind: "continued" }
|
|
53
|
+
const times = turns.map((t) => t.at).filter((t): t is string => !!t)
|
|
54
|
+
if (times[0]) h.startedAt = times[0]
|
|
55
|
+
const last = lastTime(state)
|
|
56
|
+
if (last) h.updatedAt = last
|
|
57
|
+
return { header: h, turns }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const amazonQCliReader: Reader = {
|
|
61
|
+
tool: TOOL,
|
|
62
|
+
label: LABEL,
|
|
63
|
+
async list(home) {
|
|
64
|
+
const db = dbPath(home)
|
|
65
|
+
if (!exists(db)) return []
|
|
66
|
+
try {
|
|
67
|
+
return withSnapshot(db, (d) =>
|
|
68
|
+
rows(d).map(({ key, state }): SessionRef => {
|
|
69
|
+
const title = firstPrompt(state)
|
|
70
|
+
const updatedAt = lastTime(state)
|
|
71
|
+
return { tool: TOOL, sourceId: String(state.conversation_id), path: db, cwd: key, ...(title ? { title } : {}), ...(updatedAt ? { updatedAt } : {}), extra: { key } }
|
|
72
|
+
}),
|
|
73
|
+
).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async read(ref, home) {
|
|
79
|
+
if (!exists(ref.path)) throw new Error(`Amazon Q database not found: ${ref.path}`)
|
|
80
|
+
const key = typeof ref.extra?.key === "string" ? ref.extra.key : ref.cwd
|
|
81
|
+
if (!key) throw new Error("this Amazon Q session reference names no folder")
|
|
82
|
+
return withSnapshot(ref.path, (d) => readQ(d, ref.path, key, home))
|
|
83
|
+
},
|
|
84
|
+
}
|