@chroxify/eunoe 0.1.0 → 1.1.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 (80) hide show
  1. package/README.md +364 -239
  2. package/package.json +20 -3
  3. package/src/cli/{types.ts → command.ts} +1 -4
  4. package/src/cli/commands/index.ts +27 -3
  5. package/src/cli/commands/load.ts +44 -0
  6. package/src/cli/commands/mode.ts +68 -24
  7. package/src/cli/commands/peek.ts +22 -0
  8. package/src/cli/commands/replay.ts +194 -0
  9. package/src/cli/commands/serve.ts +24 -8
  10. package/src/cli/commands/sessions.ts +70 -0
  11. package/src/cli/commands/settings.ts +119 -0
  12. package/src/cli/commands/start.ts +73 -0
  13. package/src/cli/commands/status.ts +107 -30
  14. package/src/cli/commands/stop.ts +36 -12
  15. package/src/cli/commands/uninstall.ts +14 -14
  16. package/src/cli/commands/watch.ts +16 -0
  17. package/src/cli/logo/art.ts +99 -0
  18. package/src/cli/logo/index.ts +12 -18
  19. package/src/cli/main.ts +3 -3
  20. package/src/cli/proxy/claude-settings.ts +59 -0
  21. package/src/cli/proxy/inspect.ts +67 -0
  22. package/src/cli/proxy/launchd.ts +53 -0
  23. package/src/cli/proxy/service.ts +53 -0
  24. package/src/cli/proxy/stats.ts +81 -0
  25. package/src/cli/proxy/watch.ts +60 -0
  26. package/src/cli/replay/index.ts +141 -0
  27. package/src/cli/replay/worker.ts +15 -0
  28. package/src/cli/sessions/browse.ts +88 -0
  29. package/src/cli/sessions/index.ts +76 -0
  30. package/src/cli/sessions/source.ts +74 -0
  31. package/src/cli/{args.ts → terminal/args.ts} +1 -1
  32. package/src/cli/{help.ts → terminal/help.ts} +25 -9
  33. package/src/cli/terminal/prompt.ts +114 -0
  34. package/src/cli/terminal/ui.ts +137 -0
  35. package/src/config/index.ts +128 -14
  36. package/src/config/{constants.ts → paths.ts} +4 -14
  37. package/src/context/content.ts +51 -3
  38. package/src/context/fold/evidence.ts +145 -0
  39. package/src/context/fold/index.ts +65 -0
  40. package/src/context/fold/strategies.ts +94 -0
  41. package/src/context/fold/turns.ts +76 -0
  42. package/src/context/prompts.ts +141 -22
  43. package/src/context/recall/index.ts +214 -0
  44. package/src/context/recall/rerank.ts +71 -0
  45. package/src/proxy/canonical.ts +40 -0
  46. package/src/proxy/compaction.ts +122 -52
  47. package/src/proxy/estimate.ts +14 -9
  48. package/src/proxy/fingerprint.ts +29 -0
  49. package/src/proxy/handoff.ts +71 -0
  50. package/src/proxy/log.ts +2 -2
  51. package/src/proxy/recall.ts +58 -0
  52. package/src/proxy/rewrite.ts +152 -56
  53. package/src/proxy/server.ts +122 -47
  54. package/src/proxy/sessions.ts +64 -0
  55. package/src/proxy/state.ts +17 -3
  56. package/src/proxy/usage.ts +50 -34
  57. package/src/proxy/window.ts +32 -34
  58. package/src/transcript/format.ts +88 -25
  59. package/src/transcript/locate.ts +38 -17
  60. package/src/transcript/messages.ts +18 -0
  61. package/src/transcript/parse.ts +137 -56
  62. package/src/transcript/render.ts +44 -17
  63. package/src/transcript/search.ts +20 -56
  64. package/src/transcript/subagents.ts +150 -0
  65. package/src/cli/claude-settings.ts +0 -42
  66. package/src/cli/commands/install.ts +0 -44
  67. package/src/cli/constants.ts +0 -29
  68. package/src/cli/inspect.ts +0 -41
  69. package/src/cli/launchd.ts +0 -34
  70. package/src/cli/logo/pixels.ts +0 -34
  71. package/src/cli/ui.ts +0 -91
  72. package/src/config/types.ts +0 -21
  73. package/src/context/constants.ts +0 -7
  74. package/src/context/strategies.ts +0 -89
  75. package/src/context/turns.ts +0 -82
  76. package/src/context/types.ts +0 -24
  77. package/src/proxy/constants.ts +0 -19
  78. package/src/proxy/types.ts +0 -58
  79. package/src/transcript/constants.ts +0 -16
  80. package/src/transcript/types.ts +0 -43
@@ -1,21 +0,0 @@
1
- export type Mode = "off" | "trim" | "compact"
2
- export type SearchMode = "jsonl" | "markdown" | "xml" | "qmd"
3
-
4
- export interface EvalConfig {
5
- strategy?: "tail" | "guide"
6
- tailTurns?: number
7
- forceCut?: boolean
8
- trustHeaders?: boolean
9
- }
10
-
11
- export interface Config {
12
- mode: Mode
13
- port: number
14
- compactAt: number
15
- keepTurns: number | "all"
16
- search: SearchMode
17
- upstream: string
18
- claudeConfigDirs: string[]
19
- window?: number
20
- eval?: EvalConfig
21
- }
@@ -1,7 +0,0 @@
1
- export const SUMMARY_COMMAND_CHARS = 100
2
- export const SUMMARY_ARGUMENT_CHARS = 60
3
- export const SUMMARY_LAST_WORDS_CHARS = 600
4
- export const SUMMARY_CALLS_SHOWN = 20
5
-
6
- export const CLEARED_INPUT_THRESHOLD = 2_000
7
- export const CLEARED_INPUT_KEPT = 500
@@ -1,89 +0,0 @@
1
- import { CLEARED_INPUT_KEPT, CLEARED_INPUT_THRESHOLD } from "./constants"
2
- import { blocks, withoutCacheControl } from "./content"
3
- import { compactionNotice, truncationNotice } from "./prompts"
4
- import { hasContent, isHarness, reduceTurn, splitTurns } from "./turns"
5
- import type { Block, Body, CompactOptions, Message } from "./types"
6
-
7
- export function trimMessages(messages: Message[]): Message[] {
8
- const { preamble, turns } = splitTurns(messages)
9
- if (turns.length < 2) return messages
10
- return [...preamble, ...turns.slice(0, -1).flatMap(reduceTurn), ...turns.at(-1)!]
11
- }
12
-
13
- export function compactMessages(
14
- messages: Message[],
15
- cutIndex: number,
16
- options: CompactOptions,
17
- ): Message[] {
18
- const { preamble, turns } = splitTurns(messages)
19
- if (cutIndex <= 0 || cutIndex >= turns.length) return messages
20
- const keptFrom = Math.max(1, cutIndex - options.keepTurns)
21
- const notice = compactionNotice({ omitted: keptFrom - 1 + (options.omittedBefore ?? 0), kept: cutIndex - keptFrom, guide: options.guide })
22
- const removed = [...turns[0].slice(1), ...turns.slice(1, keptFrom).flat()]
23
- const carried = removed
24
- .filter((m) => isHarness(m) && hasContent(m))
25
- .flatMap((m) => blocks(m).filter((b) => b.type === "text"))
26
- .map((b) => ({ type: "text", text: `<system-reminder>\n${b.text}\n</system-reminder>` }))
27
- return [
28
- ...preamble,
29
- turns[0][0],
30
- { role: "user", content: [{ type: "text", text: notice }, ...carried] },
31
- ...turns.slice(keptFrom, cutIndex).flatMap(reduceTurn),
32
- ...turns.slice(cutIndex).flat(),
33
- ]
34
- }
35
-
36
- export function tailMessages(messages: Message[], from: number, guide: string | null): Message[] {
37
- const { preamble, turns } = splitTurns(messages)
38
- if (turns.length === 0 || from <= 1) return messages
39
- const notice = truncationNotice({ omitted: from - 1, kept: turns.length - from, guide })
40
- return [
41
- ...preamble,
42
- turns[0][0],
43
- { role: "user", content: [{ type: "text", text: notice }] },
44
- ...turns.slice(from).flat(),
45
- ]
46
- }
47
-
48
- export function countToolResults(messages: Message[], from: number): number {
49
- return messages.slice(from).reduce((total, m) => total + (m.role === "user" ? blocks(m).filter((b) => b.type === "tool_result").length : 0), 0)
50
- }
51
-
52
- export function clearToolResults(messages: Message[], from: number, count: number, placeholder: string): Message[] {
53
- let left = count
54
- const cleared = new Set<string>()
55
- const results = messages.map((message, index) => {
56
- if (left <= 0 || index < from || message.role !== "user" || typeof message.content === "string") return message
57
- if (!message.content.some((b) => b.type === "tool_result")) return message
58
- return {
59
- ...message,
60
- content: message.content.map((b) => {
61
- if (b.type !== "tool_result" || left <= 0) return b
62
- left -= 1
63
- cleared.add(b.tool_use_id)
64
- return { type: "tool_result", tool_use_id: b.tool_use_id, ...(b.is_error ? { is_error: true } : {}), content: placeholder }
65
- }),
66
- }
67
- })
68
- return results.map((message) => {
69
- if (message.role !== "assistant" || typeof message.content === "string") return message
70
- if (!message.content.some((b) => b.type === "tool_use" && cleared.has(b.id))) return message
71
- return { ...message, content: message.content.map((b) => (b.type === "tool_use" && cleared.has(b.id) ? { ...b, input: shortenInput(b.input) } : b)) }
72
- })
73
- }
74
-
75
- function shortenInput(input: unknown): unknown {
76
- if (typeof input === "string") return input.length > CLEARED_INPUT_THRESHOLD ? `${input.slice(0, CLEARED_INPUT_KEPT)}… [${input.length - CLEARED_INPUT_KEPT} chars cleared; in the transcript]` : input
77
- if (Array.isArray(input)) return input.map(shortenInput)
78
- if (input && typeof input === "object") return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, shortenInput(v)]))
79
- return input
80
- }
81
-
82
- export function appendSystem(system: Body["system"], text: string): Block[] {
83
- const list: Block[] = typeof system === "string" ? [{ type: "text", text: system }] : [...(system ?? [])]
84
- const lastCached = list.findLastIndex((b) => b.cache_control)
85
- const cacheControl = lastCached >= 0 ? list[lastCached].cache_control : undefined
86
- if (lastCached >= 0) list[lastCached] = withoutCacheControl(list[lastCached])
87
- list.push({ type: "text", text, ...(cacheControl ? { cache_control: cacheControl } : {}) })
88
- return list
89
- }
@@ -1,82 +0,0 @@
1
- import { SUMMARY_ARGUMENT_CHARS, SUMMARY_CALLS_SHOWN, SUMMARY_COMMAND_CHARS, SUMMARY_LAST_WORDS_CHARS } from "./constants"
2
- import { blocks, clean, clip, withoutCacheControl } from "./content"
3
- import type { Block, Message } from "./types"
4
-
5
- export function isPrompt(message: Message) {
6
- if (message.role !== "user") return false
7
- const content = blocks(message)
8
- return !content.some((block) => block.type === "tool_result")
9
- && content.some((block) => block.type === "text" || block.type === "image" || block.type === "document")
10
- }
11
-
12
- export function splitTurns(messages: Message[]): { preamble: Message[]; turns: Message[][] } {
13
- const preamble: Message[] = []
14
- const turns: Message[][] = []
15
- for (const message of messages) {
16
- if (isPrompt(message) && !(turns.length > 0 && turns.at(-1)!.every((m) => m.role === "user"))) {
17
- turns.push([message])
18
- } else if (turns.length > 0) {
19
- turns.at(-1)!.push(message)
20
- } else {
21
- preamble.push(message)
22
- }
23
- }
24
- return { preamble, turns }
25
- }
26
-
27
- export function turnFingerprint(turn: Message[]): string {
28
- const hasher = new Bun.CryptoHasher("sha256")
29
- hasher.update(JSON.stringify(blocks(turn[0]).map(withoutCacheControl)))
30
- return hasher.digest("hex").slice(0, 24)
31
- }
32
-
33
- export function isHarness(message: Message) {
34
- return message.role === "system"
35
- }
36
-
37
- export function hasContent(message: Message) {
38
- return blocks(message).some((block) => block.type !== "text" || String(block.text ?? "").trim())
39
- }
40
-
41
- function mergedHarness(turn: Message[]): Message[] {
42
- const content = turn.filter((m) => isHarness(m) && hasContent(m)).flatMap((m) => blocks(m).map(withoutCacheControl))
43
- return content.length > 0 ? [{ role: "system", content }] : []
44
- }
45
-
46
- function describeToolCall(block: Block): string {
47
- const input = block.input ?? {}
48
- if (typeof input.command === "string") return `${block.name} \`${clip(input.command, SUMMARY_COMMAND_CHARS)}\``
49
- if (typeof input.file_path === "string") return `${block.name} ${input.file_path}`
50
- if (typeof input.pattern === "string") return `${block.name} "${clip(input.pattern, SUMMARY_ARGUMENT_CHARS)}"`
51
- return `${block.name} ${clip(JSON.stringify(input), SUMMARY_ARGUMENT_CHARS)}`
52
- }
53
-
54
- function interruptedSummary(turn: Message[]): string {
55
- const assistant = turn.filter((m) => m.role === "assistant")
56
- const lastWords = [...assistant].reverse()
57
- .map((m) => blocks(m).filter((b) => b.type === "text").map((b) => String(b.text ?? "")).join("\n").trim())
58
- .find(Boolean)
59
- const calls = assistant.flatMap((m) => blocks(m).filter((b) => b.type === "tool_use")).map(describeToolCall)
60
- const shown = calls.slice(-SUMMARY_CALLS_SHOWN)
61
- return [
62
- "[This turn was interrupted before a final reply.]",
63
- lastWords ? `Last thing you said: ${lastWords.length > SUMMARY_LAST_WORDS_CHARS ? `${lastWords.slice(0, SUMMARY_LAST_WORDS_CHARS)}…` : lastWords}` : "",
64
- calls.length > 0 ? `Tool calls (${calls.length}${calls.length > shown.length ? `, last ${shown.length} shown` : ""}): ${shown.join("; ")}` : "",
65
- "Their output is in the full transcript.",
66
- ].filter(Boolean).join("\n")
67
- }
68
-
69
- export function reduceTurn(turn: Message[]): Message[] {
70
- const opening: Message[] = []
71
- for (let i = 0; i < turn.length && turn[i].role !== "assistant"; i += 1) {
72
- if (turn[i].role === "user") opening.push(clean(turn[i]))
73
- }
74
- opening.push(...mergedHarness(turn))
75
- const last = [...turn].reverse().find((m) => m.role !== "system")!
76
- const content = last.role === "assistant" ? blocks(last) : []
77
- const text = content.filter((b) => b.type === "text" && String(b.text ?? "").trim())
78
- const final = text.length > 0 && !content.some((b) => b.type === "tool_use")
79
- ? text.map((b) => ({ type: "text", text: b.text }))
80
- : [{ type: "text", text: interruptedSummary(turn) }]
81
- return [...opening, { role: "assistant", content: final }]
82
- }
@@ -1,24 +0,0 @@
1
- export type Block = { type: string; [key: string]: any }
2
-
3
- export type Message = { role: string; content: string | Block[] }
4
-
5
- export type Body = {
6
- model?: string
7
- system?: string | Block[]
8
- messages: Message[]
9
- tools?: unknown[]
10
- metadata?: { user_id?: string }
11
- [key: string]: any
12
- }
13
-
14
- export interface CompactOptions {
15
- keepTurns: number
16
- guide: string | null
17
- omittedBefore?: number
18
- }
19
-
20
- export interface NoticeCounts {
21
- omitted: number
22
- kept: number
23
- guide: string | null
24
- }
@@ -1,19 +0,0 @@
1
- export const HEALTH_PATH = "/_eunoe/health"
2
- export const SESSION_HEADER = "x-claude-code-session-id"
3
- export const TRANSCRIPT_HEADER = "x-eunoe-transcript"
4
- export const OMITTED_BEFORE_HEADER = "x-eunoe-omitted-before"
5
-
6
- export const TARGET_SHARE = 0.3
7
- export const PROTECTED_OUTPUTS = 5
8
-
9
- export const DEFAULT_RATIO = 1 / 3.6
10
- export const MEDIA_CHARS = 5_800
11
- export const MEDIA_MIN_LENGTH = 2_000
12
-
13
- export const MODEL_HEADERS = ["authorization", "x-api-key", "anthropic-version", "anthropic-beta"]
14
- export const ANTHROPIC_VERSION = "2023-06-01"
15
- export const MODEL_LOOKUP_TIMEOUT_MS = 5_000
16
-
17
- export const MAX_EVENT_BUFFER = 4_000_000
18
- export const LOGGED_ERROR_CHARS = 500
19
- export const IDLE_TIMEOUT_SECONDS = 255
@@ -1,58 +0,0 @@
1
- import type { Config } from "../config/types"
2
- import type { Body, Message } from "../context/types"
3
- import type { TranscriptRef } from "../transcript/types"
4
-
5
- export interface ThreadState {
6
- cut?: string
7
- keep?: number
8
- cleared?: number
9
- ratio?: number
10
- }
11
-
12
- export type Note = Record<string, unknown>
13
-
14
- export interface RewriteInput {
15
- body: Body
16
- sessionId: string | null
17
- config: Config
18
- transcript: TranscriptRef | null
19
- window: number | null
20
- mayCompact: boolean
21
- omittedBefore?: number
22
- }
23
-
24
- export interface Rewrite {
25
- body: Body
26
- threadKey: string | null
27
- note: Note
28
- }
29
-
30
- export interface CompactInput {
31
- body: Body
32
- config: Config
33
- state: ThreadState
34
- window: number | null
35
- guide: string | null
36
- mayCompact: boolean
37
- omittedBefore?: number
38
- }
39
-
40
- export interface CompactResult {
41
- messages: Message[]
42
- stateChanged: boolean
43
- note: Note
44
- }
45
-
46
- export interface Estimator {
47
- of: (messages: Message[]) => number
48
- fixed: number
49
- }
50
-
51
- export type Usage = Record<string, number>
52
-
53
- export interface Health {
54
- ok: boolean
55
- version: string
56
- mode: Config["mode"]
57
- pid: number
58
- }
@@ -1,16 +0,0 @@
1
- export const OUTPUT_CAP = 40_000
2
-
3
- export const CALL_COMMAND_CHARS = 300
4
- export const CALL_PATTERN_CHARS = 120
5
- export const CALL_DESCRIPTION_CHARS = 160
6
- export const CALL_INPUT_CHARS = 200
7
-
8
- export const INDEX_PROMPT_CHARS = 140
9
- export const INDEX_REPLY_CHARS = 160
10
-
11
- export const RENDER_STAMP = ".rendered"
12
- export const TURN_COUNT_FILE = ".turns"
13
- export const INDEX_FILE = "index.md"
14
- export const QMD_DIR = ".qmd"
15
-
16
- export const SESSION_ID_PATTERN = /^[a-zA-Z0-9-]+$/
@@ -1,43 +0,0 @@
1
- import type { SearchMode } from "../config/types"
2
-
3
- export type SessionEntry = Record<string, any>
4
-
5
- export type TranscriptFormat = "markdown" | "xml"
6
-
7
- export interface ToolPart {
8
- kind: "tool"
9
- n: number
10
- name: string
11
- call: string
12
- body: string
13
- output: string
14
- error: boolean
15
- }
16
-
17
- export interface SayPart {
18
- kind: "say"
19
- text: string
20
- }
21
-
22
- export type TurnPart = SayPart | ToolPart
23
-
24
- export interface Turn {
25
- number: number
26
- time: string
27
- prompt: string
28
- parts: TurnPart[]
29
- final: string
30
- calls: number
31
- }
32
-
33
- export interface RenderResult {
34
- dir: string
35
- changed: boolean
36
- turns: number
37
- }
38
-
39
- export interface TranscriptRef {
40
- mode: SearchMode
41
- jsonl: string
42
- dir: string
43
- }