@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.
Files changed (88) hide show
  1. package/README.md +68 -9
  2. package/package.json +18 -2
  3. package/src/cli/{types.ts → command.ts} +0 -4
  4. package/src/cli/commands/index.ts +13 -1
  5. package/src/cli/commands/load.ts +44 -0
  6. package/src/cli/commands/mode.ts +41 -33
  7. package/src/cli/commands/peek.ts +22 -0
  8. package/src/cli/commands/replay.ts +245 -0
  9. package/src/cli/commands/serve.ts +22 -7
  10. package/src/cli/commands/sessions.ts +70 -0
  11. package/src/cli/commands/settings.ts +79 -60
  12. package/src/cli/commands/start.ts +56 -21
  13. package/src/cli/commands/stats.ts +233 -0
  14. package/src/cli/commands/status.ts +98 -55
  15. package/src/cli/commands/stop.ts +8 -6
  16. package/src/cli/commands/uninstall.ts +10 -12
  17. package/src/cli/commands/watch.ts +16 -0
  18. package/src/cli/logo/art.ts +99 -0
  19. package/src/cli/logo/index.ts +12 -18
  20. package/src/cli/main.ts +3 -3
  21. package/src/cli/proxy/claude-settings.ts +69 -0
  22. package/src/cli/proxy/inspect.ts +67 -0
  23. package/src/cli/proxy/launchd.ts +53 -0
  24. package/src/cli/proxy/live.ts +57 -0
  25. package/src/cli/proxy/service.ts +53 -0
  26. package/src/cli/{stats.ts → proxy/stats.ts} +29 -16
  27. package/src/cli/proxy/watch.ts +60 -0
  28. package/src/cli/replay/index.ts +146 -0
  29. package/src/cli/replay/worker.ts +20 -0
  30. package/src/cli/sessions/browse.ts +88 -0
  31. package/src/cli/sessions/index.ts +76 -0
  32. package/src/cli/sessions/source.ts +74 -0
  33. package/src/cli/{help.ts → terminal/help.ts} +24 -8
  34. package/src/cli/terminal/prompt.ts +114 -0
  35. package/src/cli/{ui.ts → terminal/ui.ts} +55 -24
  36. package/src/cli/usage/index.ts +151 -0
  37. package/src/config/index.ts +99 -34
  38. package/src/config/paths.ts +17 -0
  39. package/src/context/content.ts +40 -14
  40. package/src/context/fold/evidence.ts +145 -0
  41. package/src/context/fold/index.ts +65 -0
  42. package/src/context/fold/strategies.ts +94 -0
  43. package/src/context/fold/turns.ts +76 -0
  44. package/src/context/prompts.ts +141 -22
  45. package/src/context/recall/index.ts +214 -0
  46. package/src/context/recall/rerank.ts +71 -0
  47. package/src/proxy/canonical.ts +17 -9
  48. package/src/proxy/compaction.ts +106 -53
  49. package/src/proxy/estimate.ts +14 -9
  50. package/src/proxy/fingerprint.ts +23 -9
  51. package/src/proxy/handoff.ts +71 -0
  52. package/src/proxy/log.ts +2 -2
  53. package/src/proxy/recall.ts +58 -0
  54. package/src/proxy/rewrite.ts +141 -119
  55. package/src/proxy/server.ts +108 -51
  56. package/src/proxy/sessions.ts +32 -14
  57. package/src/proxy/state.ts +17 -3
  58. package/src/proxy/usage.ts +50 -34
  59. package/src/proxy/window.ts +32 -34
  60. package/src/transcript/format.ts +88 -25
  61. package/src/transcript/locate.ts +38 -17
  62. package/src/transcript/messages.ts +3 -14
  63. package/src/transcript/parse.ts +137 -56
  64. package/src/transcript/render.ts +44 -17
  65. package/src/transcript/search.ts +20 -56
  66. package/src/transcript/subagents.ts +150 -0
  67. package/src/cli/claude-settings.ts +0 -42
  68. package/src/cli/constants.ts +0 -52
  69. package/src/cli/inspect.ts +0 -41
  70. package/src/cli/launchd.ts +0 -39
  71. package/src/cli/logo/pixels.ts +0 -34
  72. package/src/cli/prompt.ts +0 -91
  73. package/src/cli/service.ts +0 -33
  74. package/src/cli/sessions.ts +0 -25
  75. package/src/config/constants.ts +0 -39
  76. package/src/config/types.ts +0 -50
  77. package/src/context/constants.ts +0 -42
  78. package/src/context/evidence.ts +0 -92
  79. package/src/context/recall.ts +0 -155
  80. package/src/context/rerank.ts +0 -44
  81. package/src/context/strategies.ts +0 -89
  82. package/src/context/turns.ts +0 -87
  83. package/src/context/types.ts +0 -66
  84. package/src/proxy/constants.ts +0 -29
  85. package/src/proxy/types.ts +0 -82
  86. package/src/transcript/constants.ts +0 -15
  87. package/src/transcript/types.ts +0 -43
  88. /package/src/cli/{args.ts → terminal/args.ts} +0 -0
@@ -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/constants"
4
- import { INDEX_FILE, RENDER_STAMP, TURN_COUNT_FILE } from "./constants"
5
- import { renderIndex, renderMarkdown, renderXml, turnFile } from "./format"
6
- import { parseTurns } from "./parse"
7
- import type { RenderResult, TranscriptFormat } from "./types"
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
- export function renderTranscript(sessionFile: string, sessionId: string, outDir = transcriptDir(sessionId), format: TranscriptFormat = "markdown"): RenderResult {
14
- mkdirSync(outDir, { recursive: true })
15
- const stampFile = path.join(outDir, RENDER_STAMP)
16
- const mtime = String(statSync(sessionFile).mtimeMs)
17
- if (existsSync(stampFile) && readFileSync(stampFile, "utf8") === mtime) {
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
- const entries = readFileSync(sessionFile, "utf8").split("\n").flatMap((line) => {
21
- try { return line ? [JSON.parse(line)] : [] } catch { return [] }
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
- const turns = parseTurns(entries)
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
- write(INDEX_FILE, renderIndex(turns, format))
33
- writeFileSync(stampFile, mtime)
34
- writeFileSync(path.join(outDir, TURN_COUNT_FILE), String(turns.length))
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
  }
@@ -1,9 +1,13 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import path from "node:path"
3
- import type { SearchMode } from "../config/types"
4
- import { QMD_DIR } from "./constants"
3
+ import type { SearchMode } from "../config"
5
4
  import { renderTranscript, transcriptDir } from "./render"
6
- import type { TranscriptRef } from "./types"
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 previous = indexing.get(dir) ?? Promise.resolve()
16
- const next = previous.then(async () => {
17
- if (!existsSync(path.join(dir, QMD_DIR))) {
18
- await qmd(dir, ["init"])
19
- await qmd(dir, ["collection", "add", ".", "--name", "transcript"])
20
- }
21
- await qmd(dir, ["update"])
22
- await qmd(dir, ["embed"])
23
- }).catch(() => undefined)
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 || !existsSync(path.join(dir, QMD_DIR)))) void indexWithQmd(dir)
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
- }
@@ -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
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }