@chroxify/eunoe 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -9
- package/package.json +18 -2
- package/src/cli/{types.ts → command.ts} +0 -4
- package/src/cli/commands/index.ts +13 -1
- package/src/cli/commands/load.ts +44 -0
- package/src/cli/commands/mode.ts +41 -33
- package/src/cli/commands/peek.ts +22 -0
- package/src/cli/commands/replay.ts +245 -0
- package/src/cli/commands/serve.ts +22 -7
- package/src/cli/commands/sessions.ts +70 -0
- package/src/cli/commands/settings.ts +79 -60
- package/src/cli/commands/start.ts +56 -21
- package/src/cli/commands/stats.ts +233 -0
- package/src/cli/commands/status.ts +98 -55
- package/src/cli/commands/stop.ts +8 -6
- package/src/cli/commands/uninstall.ts +10 -12
- package/src/cli/commands/watch.ts +16 -0
- package/src/cli/logo/art.ts +99 -0
- package/src/cli/logo/index.ts +12 -18
- package/src/cli/main.ts +3 -3
- package/src/cli/proxy/claude-settings.ts +69 -0
- package/src/cli/proxy/inspect.ts +67 -0
- package/src/cli/proxy/launchd.ts +53 -0
- package/src/cli/proxy/live.ts +57 -0
- package/src/cli/proxy/service.ts +53 -0
- package/src/cli/{stats.ts → proxy/stats.ts} +29 -16
- package/src/cli/proxy/watch.ts +60 -0
- package/src/cli/replay/index.ts +146 -0
- package/src/cli/replay/worker.ts +20 -0
- package/src/cli/sessions/browse.ts +88 -0
- package/src/cli/sessions/index.ts +76 -0
- package/src/cli/sessions/source.ts +74 -0
- package/src/cli/{help.ts → terminal/help.ts} +24 -8
- package/src/cli/terminal/prompt.ts +114 -0
- package/src/cli/{ui.ts → terminal/ui.ts} +55 -24
- package/src/cli/usage/index.ts +151 -0
- package/src/config/index.ts +99 -34
- package/src/config/paths.ts +17 -0
- package/src/context/content.ts +40 -14
- package/src/context/fold/evidence.ts +145 -0
- package/src/context/fold/index.ts +65 -0
- package/src/context/fold/strategies.ts +94 -0
- package/src/context/fold/turns.ts +76 -0
- package/src/context/prompts.ts +141 -22
- package/src/context/recall/index.ts +214 -0
- package/src/context/recall/rerank.ts +71 -0
- package/src/proxy/canonical.ts +17 -9
- package/src/proxy/compaction.ts +106 -53
- package/src/proxy/estimate.ts +14 -9
- package/src/proxy/fingerprint.ts +23 -9
- package/src/proxy/handoff.ts +71 -0
- package/src/proxy/log.ts +2 -2
- package/src/proxy/recall.ts +58 -0
- package/src/proxy/rewrite.ts +141 -119
- package/src/proxy/server.ts +108 -51
- package/src/proxy/sessions.ts +32 -14
- package/src/proxy/state.ts +17 -3
- package/src/proxy/usage.ts +50 -34
- package/src/proxy/window.ts +32 -34
- package/src/transcript/format.ts +88 -25
- package/src/transcript/locate.ts +38 -17
- package/src/transcript/messages.ts +3 -14
- package/src/transcript/parse.ts +137 -56
- package/src/transcript/render.ts +44 -17
- package/src/transcript/search.ts +20 -56
- package/src/transcript/subagents.ts +150 -0
- package/src/cli/claude-settings.ts +0 -42
- package/src/cli/constants.ts +0 -52
- package/src/cli/inspect.ts +0 -41
- package/src/cli/launchd.ts +0 -39
- package/src/cli/logo/pixels.ts +0 -34
- package/src/cli/prompt.ts +0 -91
- package/src/cli/service.ts +0 -33
- package/src/cli/sessions.ts +0 -25
- package/src/config/constants.ts +0 -39
- package/src/config/types.ts +0 -50
- package/src/context/constants.ts +0 -42
- package/src/context/evidence.ts +0 -92
- package/src/context/recall.ts +0 -155
- package/src/context/rerank.ts +0 -44
- package/src/context/strategies.ts +0 -89
- package/src/context/turns.ts +0 -87
- package/src/context/types.ts +0 -66
- package/src/proxy/constants.ts +0 -29
- package/src/proxy/types.ts +0 -82
- package/src/transcript/constants.ts +0 -15
- package/src/transcript/types.ts +0 -43
- /package/src/cli/{args.ts → terminal/args.ts} +0 -0
package/src/transcript/render.ts
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import { paths } from "../config/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { paths } from "../config/paths"
|
|
4
|
+
import { renderIndex, renderMarkdown, renderSubagentMarkdown, renderSubagentXml, renderXml, type SubagentRecord, subagentFile, turnFile } from "./format"
|
|
5
|
+
import { parseJsonl, parseTurns, type ToolPart, type TranscriptFormat, type Turn } from "./parse"
|
|
6
|
+
import { type SubagentRun, subagentEntries, subagentRuns } from "./subagents"
|
|
7
|
+
|
|
8
|
+
interface RenderResult {
|
|
9
|
+
dir: string
|
|
10
|
+
changed: boolean
|
|
11
|
+
turns: number
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
export function transcriptDir(sessionId: string) {
|
|
10
15
|
return path.join(paths.transcripts, sessionId)
|
|
11
16
|
}
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return { dir: outDir, changed: false, turns: Number(readFileSync(path.join(outDir, TURN_COUNT_FILE), "utf8") || 0) }
|
|
18
|
+
function launchingPart(turns: Turn[], run: SubagentRun): { turn: Turn; part: ToolPart } | null {
|
|
19
|
+
for (const turn of turns) {
|
|
20
|
+
for (const part of turn.parts) {
|
|
21
|
+
if (part.kind === "tool" && (run.toolUseId ? part.id === run.toolUseId : part.prompt === run.prompt)) return { turn, part }
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function subagentRecords(turns: Turn[], runs: SubagentRun[], format: TranscriptFormat): SubagentRecord[] {
|
|
28
|
+
const perTurn = new Map<number, number>()
|
|
29
|
+
return runs.map((run) => {
|
|
30
|
+
const launcher = launchingPart(turns, run)
|
|
31
|
+
const parentTurn = launcher?.turn.number ?? 0
|
|
32
|
+
const k = (perTurn.get(parentTurn) ?? 0) + 1
|
|
33
|
+
perTurn.set(parentTurn, k)
|
|
34
|
+
const file = subagentFile(parentTurn, k, format)
|
|
35
|
+
if (launcher) launcher.part.record = file
|
|
36
|
+
return { run, turns: parseTurns(subagentEntries(run), "any"), parentTurn, file }
|
|
22
37
|
})
|
|
23
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderTranscript(sessionFile: string, sessionId: string, outDir = transcriptDir(sessionId), format: TranscriptFormat = "markdown"): RenderResult {
|
|
41
|
+
mkdirSync(outDir, { recursive: true })
|
|
42
|
+
const stampFile = path.join(outDir, ".rendered")
|
|
43
|
+
const countFile = path.join(outDir, ".turns")
|
|
44
|
+
const runs = subagentRuns(sessionFile)
|
|
45
|
+
const stamp = [statSync(sessionFile).mtimeMs, ...runs.map((run) => `${run.id}:${run.mtimeMs}`)].join("|")
|
|
46
|
+
if (existsSync(stampFile) && readFileSync(stampFile, "utf8") === stamp) return { dir: outDir, changed: false, turns: Number(readFileSync(countFile, "utf8") || 0) }
|
|
47
|
+
|
|
48
|
+
const turns = parseTurns(parseJsonl(readFileSync(sessionFile, "utf8")))
|
|
49
|
+
const records = subagentRecords(turns, runs, format)
|
|
24
50
|
let changed = false
|
|
25
51
|
const write = (file: string, content: string) => {
|
|
26
52
|
const target = path.join(outDir, file)
|
|
@@ -29,8 +55,9 @@ export function renderTranscript(sessionFile: string, sessionId: string, outDir
|
|
|
29
55
|
changed = true
|
|
30
56
|
}
|
|
31
57
|
for (const turn of turns) write(turnFile(turn, format), format === "xml" ? renderXml(turn) : renderMarkdown(turn))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
writeFileSync(
|
|
58
|
+
for (const record of records) write(record.file, format === "xml" ? renderSubagentXml(record) : renderSubagentMarkdown(record))
|
|
59
|
+
write("index.md", renderIndex(turns, format, records))
|
|
60
|
+
writeFileSync(stampFile, stamp)
|
|
61
|
+
writeFileSync(countFile, String(turns.length))
|
|
35
62
|
return { dir: outDir, changed, turns: turns.length }
|
|
36
63
|
}
|
package/src/transcript/search.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import type { SearchMode } from "../config
|
|
4
|
-
import { QMD_DIR } from "./constants"
|
|
3
|
+
import type { SearchMode } from "../config"
|
|
5
4
|
import { renderTranscript, transcriptDir } from "./render"
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
export interface TranscriptRef {
|
|
7
|
+
mode: SearchMode
|
|
8
|
+
jsonl: string
|
|
9
|
+
dir: string
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
const indexing = new Map<string, Promise<void>>()
|
|
9
13
|
|
|
@@ -11,16 +15,19 @@ async function qmd(cwd: string, args: string[]) {
|
|
|
11
15
|
await Bun.spawn(["qmd", ...args], { cwd, stdout: "ignore", stderr: "ignore" }).exited
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
const isIndexed = (dir: string) => existsSync(path.join(dir, ".qmd"))
|
|
19
|
+
|
|
14
20
|
function indexWithQmd(dir: string) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const next = (indexing.get(dir) ?? Promise.resolve())
|
|
22
|
+
.then(async () => {
|
|
23
|
+
if (!isIndexed(dir)) {
|
|
24
|
+
await qmd(dir, ["init"])
|
|
25
|
+
await qmd(dir, ["collection", "add", ".", "--name", "transcript"])
|
|
26
|
+
}
|
|
27
|
+
await qmd(dir, ["update"])
|
|
28
|
+
await qmd(dir, ["embed"])
|
|
29
|
+
})
|
|
30
|
+
.catch(() => undefined)
|
|
24
31
|
indexing.set(dir, next)
|
|
25
32
|
return next
|
|
26
33
|
}
|
|
@@ -30,51 +37,8 @@ export function prepareTranscript(mode: SearchMode, sessionFile: string, key: st
|
|
|
30
37
|
if (mode !== "jsonl") {
|
|
31
38
|
try {
|
|
32
39
|
const { changed } = renderTranscript(sessionFile, key, dir, mode === "xml" ? "xml" : "markdown")
|
|
33
|
-
if (mode === "qmd" && (changed || !
|
|
40
|
+
if (mode === "qmd" && (changed || !isIndexed(dir))) void indexWithQmd(dir)
|
|
34
41
|
} catch {}
|
|
35
42
|
}
|
|
36
43
|
return { mode, jsonl: sessionFile, dir }
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
export function searchGuide(ref: TranscriptRef): string {
|
|
40
|
-
if (ref.mode === "jsonl") {
|
|
41
|
-
return [
|
|
42
|
-
`The full transcript is Claude Code's session file: ${ref.jsonl}`,
|
|
43
|
-
"It is JSONL, one entry per line. Your tool calls are `tool_use` blocks in `assistant` entries; their output is in the `tool_result` blocks of the `user` entries that follow. It is large: search it narrowly and never read it whole. For example:",
|
|
44
|
-
` grep -n 'distinctive text' ${ref.jsonl} | cut -c1-400`,
|
|
45
|
-
` jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | "\\(.name) \\(.input | tostring | .[0:200])"' ${ref.jsonl}`,
|
|
46
|
-
].join("\n")
|
|
47
|
-
}
|
|
48
|
-
const ext = ref.mode === "xml" ? "xml" : "md"
|
|
49
|
-
const layout = ref.mode === "xml"
|
|
50
|
-
? [
|
|
51
|
-
`The full transcript is a folder with one XML file per turn: ${ref.dir}`,
|
|
52
|
-
"- `index.md`: one line per turn, what the user asked and how the turn ended. Skim it to find the turn you need.",
|
|
53
|
-
"- `turn-0001.xml`, …: one turn each, `<turn n>` with `<user>` (the message), `<said>` (what you wrote while working), `<tool n name>` holding `<call>`, `<input>` and `<output>`, and `<final_reply>`.",
|
|
54
|
-
]
|
|
55
|
-
: [
|
|
56
|
-
`The full transcript is a folder of markdown files: ${ref.dir}`,
|
|
57
|
-
"- `index.md`: one line per turn, what the user asked and how the turn ended. Skim it to find the turn you need.",
|
|
58
|
-
"- `turn-0001.md`, …: one turn each. `## User` (the message), `### [tool 3/12] Bash: …` for every tool call with `[output of tool 3]` below it, and `## Final reply`.",
|
|
59
|
-
]
|
|
60
|
-
const habits = "Search for more than one phrasing (the exact term, a synonym, a file or function name, an error string) before you conclude something isn't there. Read the whole turn file around a hit: the answer is often a few calls before or after it."
|
|
61
|
-
if (ref.mode === "markdown" || ref.mode === "xml") {
|
|
62
|
-
return [
|
|
63
|
-
...layout,
|
|
64
|
-
"To find something:",
|
|
65
|
-
` grep -ril 'distinctive words' ${ref.dir} # which turns mention it`,
|
|
66
|
-
` grep -n -i 'distinctive words' ${ref.dir}/*.${ext} | cut -c1-300`,
|
|
67
|
-
` then Read ${ref.dir}/turn-NNNN.${ext}`,
|
|
68
|
-
habits,
|
|
69
|
-
].join("\n")
|
|
70
|
-
}
|
|
71
|
-
return [
|
|
72
|
-
...layout,
|
|
73
|
-
"It is indexed with qmd. Run qmd from inside that folder:",
|
|
74
|
-
` cd ${ref.dir} && qmd query "a natural-language question" # semantic + keyword, best when you don't know the exact words (5–20s)`,
|
|
75
|
-
` cd ${ref.dir} && qmd search "exact words" # keyword only, instant`,
|
|
76
|
-
` then Read ${ref.dir}/turn-NNNN.md (or qmd get turn-NNNN.md)`,
|
|
77
|
-
"If qmd isn't ready yet (the index builds in the background), grep the folder instead.",
|
|
78
|
-
habits,
|
|
79
|
-
].join("\n")
|
|
80
|
-
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { blocks, clip, contentBlocks, type Message } from "../context/content"
|
|
4
|
+
import { splitTurns } from "../context/fold/turns"
|
|
5
|
+
import { type RecallChunk, recallChunks } from "../context/recall"
|
|
6
|
+
import { listEntries } from "./locate"
|
|
7
|
+
import { sessionMessages } from "./messages"
|
|
8
|
+
import { entryMessages, parseJsonl, type SessionEntry } from "./parse"
|
|
9
|
+
|
|
10
|
+
export interface SubagentRun {
|
|
11
|
+
id: string
|
|
12
|
+
file: string
|
|
13
|
+
inline: boolean
|
|
14
|
+
description: string
|
|
15
|
+
toolUseId: string | null
|
|
16
|
+
prompt: string
|
|
17
|
+
started: string
|
|
18
|
+
mtimeMs: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const INLINE_ID = "sidechain"
|
|
22
|
+
|
|
23
|
+
const entryCache = new Map<string, { mtimeMs: number; entries: SessionEntry[] }>()
|
|
24
|
+
const chunkCache = new Map<string, { mtimeMs: number; parentTurn: number; chunks: RecallChunk[] }>()
|
|
25
|
+
|
|
26
|
+
function mtimeOf(file: string): number | null {
|
|
27
|
+
try {
|
|
28
|
+
return statSync(file).mtimeMs
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readEntries(file: string): SessionEntry[] {
|
|
35
|
+
const mtimeMs = mtimeOf(file)
|
|
36
|
+
if (mtimeMs === null) return []
|
|
37
|
+
const hit = entryCache.get(file)
|
|
38
|
+
if (hit?.mtimeMs === mtimeMs) return hit.entries
|
|
39
|
+
const entries = parseJsonl(readFileSync(file, "utf8"))
|
|
40
|
+
entryCache.set(file, { mtimeMs, entries })
|
|
41
|
+
return entries
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function firstPrompt(entries: SessionEntry[]): string {
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.type !== "user" || !entry.message) continue
|
|
47
|
+
const text = contentBlocks(entry.message.content)
|
|
48
|
+
.filter((block) => block.type === "text")
|
|
49
|
+
.map((block) => String(block.text ?? ""))
|
|
50
|
+
.join("\n")
|
|
51
|
+
.trim()
|
|
52
|
+
if (text) return text
|
|
53
|
+
}
|
|
54
|
+
return ""
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readMeta(file: string): { description?: string; toolUseId?: string } {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(file.replace(/\.jsonl$/, ".meta.json"), "utf8"))
|
|
60
|
+
} catch {
|
|
61
|
+
return {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function subagentsDir(sessionFile: string) {
|
|
66
|
+
return path.join(path.dirname(sessionFile), path.basename(sessionFile, ".jsonl"), "subagents")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fileRuns(sessionFile: string): SubagentRun[] {
|
|
70
|
+
const dir = subagentsDir(sessionFile)
|
|
71
|
+
return listEntries(dir)
|
|
72
|
+
.filter((name) => name.startsWith("agent-") && name.endsWith(".jsonl"))
|
|
73
|
+
.map((name) => {
|
|
74
|
+
const file = path.join(dir, name)
|
|
75
|
+
const entries = readEntries(file)
|
|
76
|
+
const meta = readMeta(file)
|
|
77
|
+
const prompt = firstPrompt(entries)
|
|
78
|
+
return {
|
|
79
|
+
id: name.slice("agent-".length, -".jsonl".length),
|
|
80
|
+
file,
|
|
81
|
+
inline: false,
|
|
82
|
+
description: meta.description ? String(meta.description) : clip(prompt, 80),
|
|
83
|
+
toolUseId: meta.toolUseId ? String(meta.toolUseId) : null,
|
|
84
|
+
prompt,
|
|
85
|
+
started: String(entries[0]?.timestamp ?? ""),
|
|
86
|
+
mtimeMs: mtimeOf(file) ?? 0,
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inlineRuns(sessionFile: string, known: Set<string>): SubagentRun[] {
|
|
92
|
+
const byAgent = new Map<string, SessionEntry[]>()
|
|
93
|
+
for (const entry of readEntries(sessionFile)) {
|
|
94
|
+
if (!entry.isSidechain) continue
|
|
95
|
+
const id = entry.agentId ?? INLINE_ID
|
|
96
|
+
byAgent.set(id, [...(byAgent.get(id) ?? []), entry])
|
|
97
|
+
}
|
|
98
|
+
return [...byAgent]
|
|
99
|
+
.filter(([id]) => !known.has(id))
|
|
100
|
+
.map(([id, entries]) => {
|
|
101
|
+
const prompt = firstPrompt(entries)
|
|
102
|
+
return { id, file: sessionFile, inline: true, description: clip(prompt, 80), toolUseId: null, prompt, started: String(entries[0]?.timestamp ?? ""), mtimeMs: mtimeOf(sessionFile) ?? 0 }
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function subagentRuns(sessionFile: string): SubagentRun[] {
|
|
107
|
+
const files = fileRuns(sessionFile)
|
|
108
|
+
const inline = inlineRuns(sessionFile, new Set(files.map((run) => run.id)))
|
|
109
|
+
return [...files, ...inline].sort((a, b) => a.started.localeCompare(b.started))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function subagentEntries(run: SubagentRun): SessionEntry[] {
|
|
113
|
+
const entries = readEntries(run.file)
|
|
114
|
+
if (!run.inline) return entries
|
|
115
|
+
return entries.filter((entry) => entry.isSidechain && (entry.agentId ?? INLINE_ID) === run.id)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function subagentMessages(run: SubagentRun): Message[] {
|
|
119
|
+
return entryMessages(subagentEntries(run))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function launchedBy(run: SubagentRun, call: { id?: unknown; name?: unknown; prompt?: unknown }): boolean {
|
|
123
|
+
if (run.toolUseId) return call.id === run.toolUseId
|
|
124
|
+
return (call.name === "Agent" || call.name === "Task") && String(call.prompt ?? "") === run.prompt
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parentTurnOf(run: SubagentRun, parentMessages: Message[]): number {
|
|
128
|
+
const { turns } = splitTurns(parentMessages)
|
|
129
|
+
const index = turns.findIndex((turn) =>
|
|
130
|
+
turn
|
|
131
|
+
.filter((message) => message.role === "assistant")
|
|
132
|
+
.flatMap(blocks)
|
|
133
|
+
.some((block) => block.type === "tool_use" && launchedBy(run, { id: block.id, name: block.name, prompt: block.input?.prompt })),
|
|
134
|
+
)
|
|
135
|
+
return index + 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function subagentChunks(sessionFile: string): RecallChunk[] {
|
|
139
|
+
const parent = sessionMessages(sessionFile)
|
|
140
|
+
return subagentRuns(sessionFile).flatMap((run) => {
|
|
141
|
+
const key = `${run.file}#${run.id}`
|
|
142
|
+
const parentTurn = parentTurnOf(run, parent)
|
|
143
|
+
const hit = chunkCache.get(key)
|
|
144
|
+
if (hit && hit.mtimeMs === run.mtimeMs && hit.parentTurn === parentTurn) return hit.chunks
|
|
145
|
+
const from = `subagent "${run.description}"`
|
|
146
|
+
const chunks = recallChunks([...subagentMessages(run), { role: "user", content: "" }], new Map()).map((chunk) => ({ ...chunk, from, turn: parentTurn }))
|
|
147
|
+
chunkCache.set(key, { mtimeMs: run.mtimeMs, parentTurn, chunks })
|
|
148
|
+
return chunks
|
|
149
|
+
})
|
|
150
|
+
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import type { Settings } from "./types"
|
|
4
|
-
|
|
5
|
-
export const proxyUrl = (port: number) => `http://127.0.0.1:${port}`
|
|
6
|
-
|
|
7
|
-
export function withEunoe(settings: Settings, port: number, mode: string): Settings {
|
|
8
|
-
const next = { ...settings, env: { ...settings.env, ANTHROPIC_BASE_URL: proxyUrl(port) } }
|
|
9
|
-
return withCompactionOwner(next, mode)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function withoutEunoe(settings: Settings, port: number): Settings {
|
|
13
|
-
const next = { ...settings }
|
|
14
|
-
if (next.env?.ANTHROPIC_BASE_URL === proxyUrl(port)) {
|
|
15
|
-
const { ANTHROPIC_BASE_URL: _, ...env } = next.env
|
|
16
|
-
if (Object.keys(env).length > 0) next.env = env
|
|
17
|
-
else delete next.env
|
|
18
|
-
}
|
|
19
|
-
delete next.autoCompactEnabled
|
|
20
|
-
return next
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function withCompactionOwner(settings: Settings, mode: string): Settings {
|
|
24
|
-
const next = { ...settings }
|
|
25
|
-
if (mode === "off") delete next.autoCompactEnabled
|
|
26
|
-
else next.autoCompactEnabled = false
|
|
27
|
-
return next
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function editSettings(dirs: string[], edit: (settings: Settings) => Settings): string[] {
|
|
31
|
-
const touched: string[] = []
|
|
32
|
-
for (const dir of dirs) {
|
|
33
|
-
if (!existsSync(dir)) continue
|
|
34
|
-
const file = path.join(dir, "settings.json")
|
|
35
|
-
const current: Settings = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {}
|
|
36
|
-
const next = edit(current)
|
|
37
|
-
if (JSON.stringify(next) === JSON.stringify(current)) continue
|
|
38
|
-
writeFileSync(file, JSON.stringify(next, null, 2) + "\n")
|
|
39
|
-
touched.push(file)
|
|
40
|
-
}
|
|
41
|
-
return touched
|
|
42
|
-
}
|
package/src/cli/constants.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import type { Mode, Tunable } from "../config/types"
|
|
4
|
-
import type { OptionRow } from "./types"
|
|
5
|
-
|
|
6
|
-
export const TAGLINE = "Context management for Claude Code that never forgets what matters."
|
|
7
|
-
|
|
8
|
-
export const MODE_DESCRIPTIONS: Record<Mode, string> = {
|
|
9
|
-
default: "Recent turns whole, older turns folded; evidence and recall on",
|
|
10
|
-
compact: "Claude Code as it is, only its compaction replaced",
|
|
11
|
-
off: "Passthrough; Claude Code compacts on its own",
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const TUNABLE_DESCRIPTIONS: Record<Tunable, string> = {
|
|
15
|
-
keep: "Turns kept whole at the end, in default mode",
|
|
16
|
-
budget: "Tokens of older detail that trigger a fold, in default mode",
|
|
17
|
-
compactAt: "Share of the context window that triggers the cut",
|
|
18
|
-
keepTurns: "Earlier turns kept after a cut",
|
|
19
|
-
search: "How transcripts are written for the agent to search",
|
|
20
|
-
rerank: "Typesafe model that reorders recall, or off",
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const TUNABLE_HINTS: Record<Tunable, string> = {
|
|
24
|
-
keep: "a whole number, 1 or more",
|
|
25
|
-
budget: "a token count, 1000 or more",
|
|
26
|
-
compactAt: "a number above 0, up to 1",
|
|
27
|
-
keepTurns: "a whole number or all",
|
|
28
|
-
search: "markdown, xml, jsonl or qmd",
|
|
29
|
-
rerank: "a model name such as jev-preview, or off",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const GLOBAL_OPTIONS: OptionRow[] = [
|
|
33
|
-
["-h, --help", "Show help for a command"],
|
|
34
|
-
["-v, --version", "Show the version"],
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
export const LAUNCHD_LABEL = "com.chroxify.eunoe"
|
|
38
|
-
export const LAUNCH_AGENT_PLIST = path.join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`)
|
|
39
|
-
|
|
40
|
-
export const PROBE_TIMEOUT_MS = 800
|
|
41
|
-
export const RECENT_REQUESTS = 5_000
|
|
42
|
-
export const STATUS_WINDOW_MS = 24 * 3_600_000
|
|
43
|
-
export const TYPO_DISTANCE = 2
|
|
44
|
-
export const SHORT_ID_LENGTH = 8
|
|
45
|
-
export const SESSIONS_SHOWN = 5
|
|
46
|
-
|
|
47
|
-
export const TRANSPARENT_PIXEL = "."
|
|
48
|
-
export const HALF_BLOCKS = " ▀▄█"
|
|
49
|
-
export const OCTANTS = [..." 🮂▘▝▀🮅▖▌▞▛▗▚▐▜▂▄▙▟▆█"]
|
|
50
|
-
export const OCTANT_TERMINALS = ["ghostty"]
|
|
51
|
-
export const START_TIMEOUT_MS = 3_000
|
|
52
|
-
export const START_POLL_MS = 150
|
package/src/cli/inspect.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import { claudeConfigDirs } from "../config"
|
|
4
|
-
import { paths } from "../config/constants"
|
|
5
|
-
import type { Config } from "../config/types"
|
|
6
|
-
import { HEALTH_PATH } from "../proxy/constants"
|
|
7
|
-
import type { Health } from "../proxy/types"
|
|
8
|
-
import { proxyUrl } from "./claude-settings"
|
|
9
|
-
import { PROBE_TIMEOUT_MS, RECENT_REQUESTS } from "./constants"
|
|
10
|
-
import type { RequestRecord } from "./types"
|
|
11
|
-
|
|
12
|
-
export async function probe(port: number): Promise<Health | null> {
|
|
13
|
-
try {
|
|
14
|
-
const response = await fetch(`${proxyUrl(port)}${HEALTH_PATH}`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) })
|
|
15
|
-
return response.ok ? await response.json() as Health : null
|
|
16
|
-
} catch {
|
|
17
|
-
return null
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function routedDirs(config: Config) {
|
|
22
|
-
return claudeConfigDirs(config).filter((dir) => {
|
|
23
|
-
try {
|
|
24
|
-
const settings = JSON.parse(readFileSync(path.join(dir, "settings.json"), "utf8"))
|
|
25
|
-
return settings.env?.ANTHROPIC_BASE_URL === proxyUrl(config.port)
|
|
26
|
-
} catch {
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function recentRequests(limit = RECENT_REQUESTS): RequestRecord[] {
|
|
33
|
-
if (!existsSync(paths.requests)) return []
|
|
34
|
-
return readFileSync(paths.requests, "utf8").trim().split("\n").slice(-limit).flatMap((entry) => {
|
|
35
|
-
try {
|
|
36
|
-
return [JSON.parse(entry)]
|
|
37
|
-
} catch {
|
|
38
|
-
return []
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
}
|
package/src/cli/launchd.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import { LAUNCH_AGENT_PLIST as PLIST, LAUNCHD_LABEL } from "./constants"
|
|
4
|
-
|
|
5
|
-
export const supported = process.platform === "darwin"
|
|
6
|
-
|
|
7
|
-
export function isInstalled() {
|
|
8
|
-
return existsSync(PLIST)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function installAgent(program: string[], logFile: string) {
|
|
12
|
-
mkdirSync(path.dirname(PLIST), { recursive: true })
|
|
13
|
-
const args = program.map((arg) => `<string>${arg}</string>`).join("")
|
|
14
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
15
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
16
|
-
<plist version="1.0"><dict>
|
|
17
|
-
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
18
|
-
<key>ProgramArguments</key><array>${args}</array>
|
|
19
|
-
<key>RunAtLoad</key><true/>
|
|
20
|
-
<key>KeepAlive</key><true/>
|
|
21
|
-
<key>StandardOutPath</key><string>${logFile}</string>
|
|
22
|
-
<key>StandardErrorPath</key><string>${logFile}</string>
|
|
23
|
-
</dict></plist>
|
|
24
|
-
`
|
|
25
|
-
if (existsSync(PLIST) && readFileSync(PLIST, "utf8") === plist) {
|
|
26
|
-
Bun.spawnSync(["launchctl", "load", PLIST])
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
writeFileSync(PLIST, plist)
|
|
30
|
-
Bun.spawnSync(["launchctl", "unload", PLIST])
|
|
31
|
-
Bun.spawnSync(["launchctl", "load", PLIST])
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function removeAgent() {
|
|
35
|
-
if (!existsSync(PLIST)) return false
|
|
36
|
-
Bun.spawnSync(["launchctl", "unload", PLIST])
|
|
37
|
-
unlinkSync(PLIST)
|
|
38
|
-
return true
|
|
39
|
-
}
|
package/src/cli/logo/pixels.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export const LOGO_KEYS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456"
|
|
2
|
-
|
|
3
|
-
export const LOGO_PALETTE = ["#fefefd", "#fdfcf1", "#fef7db", "#fef7da", "#fdf7dc", "#fef7d9", "#fdf6d9", "#fef6d6", "#fdf6d7", "#fdf4d5", "#fdf6cf", "#fef4ae", "#fde9a5", "#feec97", "#fde186", "#fddb6b", "#fdd763", "#fdd661", "#fdd55d", "#fcd259", "#fdcd49", "#fdc524", "#f6f5de", "#f5e6b1", "#d7ecc9", "#e2d596", "#e5b93b", "#7ce5d0", "#68dbd4", "#74cdd0", "#52d1cc", "#33c4c9", "#2bc0c7", "#2abcc5", "#29bbc4", "#27bcc5", "#20bccc", "#27bac3", "#24b7bc", "#12b3d0", "#0baccd", "#0fa7b4", "#7f7870", "#0678a5", "#038abb", "#04527e", "#018cd0", "#016b95", "#014b85", "#01417b", "#023d73", "#013a71", "#033b6a", "#023768", "#013367", "#092945", "#002d56", "#071a2c", "#041223"]
|
|
4
|
-
|
|
5
|
-
export const LOGO_PIXELS = [
|
|
6
|
-
"..............W4...........a",
|
|
7
|
-
".............Xutt..........U",
|
|
8
|
-
".........tt...kuut.......uu.",
|
|
9
|
-
"........tkr...1Yxt4tT....u..",
|
|
10
|
-
"...XXW..soq...X2Xtst0.......",
|
|
11
|
-
"..TqssW.xr6...0CJRXs4.......",
|
|
12
|
-
"..oCCCsW.srrr012S4R1s.1.....",
|
|
13
|
-
"..kCCPrsV.ttslRSRJCFEB0m....",
|
|
14
|
-
"...pCPPrqW4W43nJJJJJHCG0....",
|
|
15
|
-
"...koCPR0sppHEtEILIHHHGhTrq.",
|
|
16
|
-
"....oCOSSZSFFEFFIIIHHHhhkEp.",
|
|
17
|
-
"XXX..nyEVPByEEEGGIIIHHLbwLo.",
|
|
18
|
-
"YrrYYY04WMGFEEggdhIIHHH63Vx.",
|
|
19
|
-
".rqnnnxKKLwFifg66hhHHGJ6ujTe",
|
|
20
|
-
".ZrpoCNSLKkGfh3355fffB4ejmj.",
|
|
21
|
-
"..1qpCY1PMLFfg3553hhhh.ihgRw",
|
|
22
|
-
".....ZZ2POJGdd653AdcgQQhfe..",
|
|
23
|
-
".....ZZPZPJdxjj6vcddcAAffT..",
|
|
24
|
-
".CC.YpCE2VhGkjmmfdcddghdT..A",
|
|
25
|
-
"....Xpp1ZVRVxhhfgddfffTT..Ab",
|
|
26
|
-
".....1Z2pC02VxxkxxgnmzA....6",
|
|
27
|
-
".......qrp2ewVPMxxmmavvv6..A",
|
|
28
|
-
"..UU...YXY..PPFkEkkvvaaavA..",
|
|
29
|
-
".UUCU......XXPMCkkvalanxavvN",
|
|
30
|
-
".UCyUNN..1Yu4PMP055nlalxBavN",
|
|
31
|
-
"..UU.NN.1uD2PwJ0MP33nlmyBuav",
|
|
32
|
-
"QyDy3y..uo2RRwFPPPP6nnBBavaa",
|
|
33
|
-
"DQQQDyy.tB2RVPPPSR4poBBaaB6N",
|
|
34
|
-
]
|
package/src/cli/prompt.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { c, useColor } from "./ui"
|
|
2
|
-
|
|
3
|
-
export interface Choice<T extends string = string> {
|
|
4
|
-
value: T
|
|
5
|
-
label?: string
|
|
6
|
-
hint?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
10
|
-
|
|
11
|
-
function keys(onKey: (key: string) => boolean | void): Promise<void> {
|
|
12
|
-
return new Promise((resolve) => {
|
|
13
|
-
const stdin = process.stdin
|
|
14
|
-
stdin.setRawMode(true)
|
|
15
|
-
stdin.resume()
|
|
16
|
-
stdin.setEncoding("utf8")
|
|
17
|
-
const handler = (chunk: string) => {
|
|
18
|
-
if (onKey(chunk) === true) {
|
|
19
|
-
stdin.off("data", handler)
|
|
20
|
-
stdin.setRawMode(false)
|
|
21
|
-
stdin.pause()
|
|
22
|
-
resolve()
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
stdin.on("data", handler)
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function render(title: string, choices: Choice[], index: number, width: number) {
|
|
30
|
-
const lines = [` ${c.bold(title)}`, ""]
|
|
31
|
-
for (const [i, choice] of choices.entries()) {
|
|
32
|
-
const label = (choice.label ?? choice.value).padEnd(width)
|
|
33
|
-
const marker = i === index ? c.green("●") : c.dim("○")
|
|
34
|
-
lines.push(` ${marker} ${i === index ? c.bold(label) : label}${choice.hint ? ` ${c.dim(choice.hint)}` : ""}`)
|
|
35
|
-
}
|
|
36
|
-
lines.push("", ` ${c.dim("↑↓ move · enter choose · esc cancel")}`)
|
|
37
|
-
return lines
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function select<T extends string>(title: string, choices: Choice<T>[], current?: T): Promise<T | null> {
|
|
41
|
-
let index = Math.max(0, choices.findIndex((choice) => choice.value === current))
|
|
42
|
-
const width = Math.max(...choices.map((choice) => (choice.label ?? choice.value).length))
|
|
43
|
-
let drawn = 0
|
|
44
|
-
const draw = () => {
|
|
45
|
-
const lines = render(title, choices, index, width)
|
|
46
|
-
if (drawn) process.stdout.write(`\x1b[${drawn}A\x1b[J`)
|
|
47
|
-
process.stdout.write(lines.join("\n") + "\n")
|
|
48
|
-
drawn = lines.length
|
|
49
|
-
}
|
|
50
|
-
let chosen: T | null = null
|
|
51
|
-
process.stdout.write(useColor ? "\x1b[?25l" : "")
|
|
52
|
-
draw()
|
|
53
|
-
await keys((key) => {
|
|
54
|
-
if (key === "\x1b[A" || key === "k") index = (index + choices.length - 1) % choices.length
|
|
55
|
-
else if (key === "\x1b[B" || key === "j") index = (index + 1) % choices.length
|
|
56
|
-
else if (key === "\r" || key === "\n") {
|
|
57
|
-
chosen = choices[index].value
|
|
58
|
-
return true
|
|
59
|
-
} else if (key === "\x1b" || key === "q" || key === "\x03") return true
|
|
60
|
-
draw()
|
|
61
|
-
})
|
|
62
|
-
process.stdout.write(`\x1b[${drawn}A\x1b[J`)
|
|
63
|
-
process.stdout.write(useColor ? "\x1b[?25h" : "")
|
|
64
|
-
return chosen
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function input(title: string, placeholder: string, initial = ""): Promise<string | null> {
|
|
68
|
-
let value = initial
|
|
69
|
-
let drawn = 0
|
|
70
|
-
const draw = () => {
|
|
71
|
-
const shown = value ? value : c.dim(placeholder)
|
|
72
|
-
const lines = [` ${c.bold(title)}`, ` ${c.cyan("›")} ${shown}`, "", ` ${c.dim("enter save · esc cancel")}`]
|
|
73
|
-
if (drawn) process.stdout.write(`\x1b[${drawn}A\x1b[J`)
|
|
74
|
-
process.stdout.write(lines.join("\n") + "\n")
|
|
75
|
-
drawn = lines.length
|
|
76
|
-
}
|
|
77
|
-
let result: string | null = null
|
|
78
|
-
draw()
|
|
79
|
-
await keys((key) => {
|
|
80
|
-
if (key === "\r" || key === "\n") {
|
|
81
|
-
result = value
|
|
82
|
-
return true
|
|
83
|
-
}
|
|
84
|
-
if (key === "\x1b" || key === "\x03") return true
|
|
85
|
-
if (key === "\x7f" || key === "\b") value = value.slice(0, -1)
|
|
86
|
-
else if (key >= " " && !key.startsWith("\x1b")) value += key
|
|
87
|
-
draw()
|
|
88
|
-
})
|
|
89
|
-
process.stdout.write(`\x1b[${drawn}A\x1b[J`)
|
|
90
|
-
return result
|
|
91
|
-
}
|
package/src/cli/service.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { paths } from "../config/constants"
|
|
3
|
-
import type { Config } from "../config/types"
|
|
4
|
-
import { proxyUrl } from "./claude-settings"
|
|
5
|
-
import { START_POLL_MS, START_TIMEOUT_MS } from "./constants"
|
|
6
|
-
import { probe } from "./inspect"
|
|
7
|
-
import * as launchd from "./launchd"
|
|
8
|
-
import { c, line, ok, warn } from "./ui"
|
|
9
|
-
|
|
10
|
-
async function waitForProxy(port: number) {
|
|
11
|
-
const deadline = Date.now() + START_TIMEOUT_MS
|
|
12
|
-
while (Date.now() < deadline) {
|
|
13
|
-
if (await probe(port)) return true
|
|
14
|
-
await Bun.sleep(START_POLL_MS)
|
|
15
|
-
}
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function startService(config: Config) {
|
|
20
|
-
if (!launchd.supported) {
|
|
21
|
-
if (await probe(config.port)) return true
|
|
22
|
-
warn("No launchd here, so keep the proxy running yourself:")
|
|
23
|
-
line(` ${c.cyan("eunoe serve")}`)
|
|
24
|
-
return false
|
|
25
|
-
}
|
|
26
|
-
launchd.installAgent([process.execPath, path.join(import.meta.dir, "main.ts"), "serve"], paths.proxyLog)
|
|
27
|
-
if (await waitForProxy(config.port)) {
|
|
28
|
-
ok(`Proxy running in the background on ${c.cyan(proxyUrl(config.port))}`)
|
|
29
|
-
return true
|
|
30
|
-
}
|
|
31
|
-
warn(`The proxy didn't answer on ${proxyUrl(config.port)} yet. Check ${paths.proxyLog}.`)
|
|
32
|
-
return false
|
|
33
|
-
}
|