@chroxify/eunoe 1.0.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.
- package/README.md +55 -9
- package/package.json +18 -2
- package/src/cli/{types.ts → command.ts} +0 -4
- package/src/cli/commands/index.ts +11 -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 +194 -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 +27 -18
- 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 +59 -0
- package/src/cli/proxy/inspect.ts +67 -0
- package/src/cli/proxy/launchd.ts +53 -0
- package/src/cli/proxy/service.ts +53 -0
- package/src/cli/{stats.ts → proxy/stats.ts} +23 -15
- package/src/cli/proxy/watch.ts +60 -0
- package/src/cli/replay/index.ts +141 -0
- package/src/cli/replay/worker.ts +15 -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/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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const HALF_BLOCKS = " ▀▄█"
|
|
2
|
+
export const OCTANTS = [
|
|
3
|
+
..." 🮂▘▝▀🮅▖▌▞▛▗▚▐▜▂▄▙▟▆█",
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
export const KEYS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456"
|
|
7
|
+
|
|
8
|
+
export const PALETTE = [
|
|
9
|
+
"#fefefd",
|
|
10
|
+
"#fdfcf1",
|
|
11
|
+
"#fef7db",
|
|
12
|
+
"#fef7da",
|
|
13
|
+
"#fdf7dc",
|
|
14
|
+
"#fef7d9",
|
|
15
|
+
"#fdf6d9",
|
|
16
|
+
"#fef6d6",
|
|
17
|
+
"#fdf6d7",
|
|
18
|
+
"#fdf4d5",
|
|
19
|
+
"#fdf6cf",
|
|
20
|
+
"#fef4ae",
|
|
21
|
+
"#fde9a5",
|
|
22
|
+
"#feec97",
|
|
23
|
+
"#fde186",
|
|
24
|
+
"#fddb6b",
|
|
25
|
+
"#fdd763",
|
|
26
|
+
"#fdd661",
|
|
27
|
+
"#fdd55d",
|
|
28
|
+
"#fcd259",
|
|
29
|
+
"#fdcd49",
|
|
30
|
+
"#fdc524",
|
|
31
|
+
"#f6f5de",
|
|
32
|
+
"#f5e6b1",
|
|
33
|
+
"#d7ecc9",
|
|
34
|
+
"#e2d596",
|
|
35
|
+
"#e5b93b",
|
|
36
|
+
"#7ce5d0",
|
|
37
|
+
"#68dbd4",
|
|
38
|
+
"#74cdd0",
|
|
39
|
+
"#52d1cc",
|
|
40
|
+
"#33c4c9",
|
|
41
|
+
"#2bc0c7",
|
|
42
|
+
"#2abcc5",
|
|
43
|
+
"#29bbc4",
|
|
44
|
+
"#27bcc5",
|
|
45
|
+
"#20bccc",
|
|
46
|
+
"#27bac3",
|
|
47
|
+
"#24b7bc",
|
|
48
|
+
"#12b3d0",
|
|
49
|
+
"#0baccd",
|
|
50
|
+
"#0fa7b4",
|
|
51
|
+
"#7f7870",
|
|
52
|
+
"#0678a5",
|
|
53
|
+
"#038abb",
|
|
54
|
+
"#04527e",
|
|
55
|
+
"#018cd0",
|
|
56
|
+
"#016b95",
|
|
57
|
+
"#014b85",
|
|
58
|
+
"#01417b",
|
|
59
|
+
"#023d73",
|
|
60
|
+
"#013a71",
|
|
61
|
+
"#033b6a",
|
|
62
|
+
"#023768",
|
|
63
|
+
"#013367",
|
|
64
|
+
"#092945",
|
|
65
|
+
"#002d56",
|
|
66
|
+
"#071a2c",
|
|
67
|
+
"#041223",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
export const PIXELS = [
|
|
71
|
+
"..............W4...........a",
|
|
72
|
+
".............Xutt..........U",
|
|
73
|
+
".........tt...kuut.......uu.",
|
|
74
|
+
"........tkr...1Yxt4tT....u..",
|
|
75
|
+
"...XXW..soq...X2Xtst0.......",
|
|
76
|
+
"..TqssW.xr6...0CJRXs4.......",
|
|
77
|
+
"..oCCCsW.srrr012S4R1s.1.....",
|
|
78
|
+
"..kCCPrsV.ttslRSRJCFEB0m....",
|
|
79
|
+
"...pCPPrqW4W43nJJJJJHCG0....",
|
|
80
|
+
"...koCPR0sppHEtEILIHHHGhTrq.",
|
|
81
|
+
"....oCOSSZSFFEFFIIIHHHhhkEp.",
|
|
82
|
+
"XXX..nyEVPByEEEGGIIIHHLbwLo.",
|
|
83
|
+
"YrrYYY04WMGFEEggdhIIHHH63Vx.",
|
|
84
|
+
".rqnnnxKKLwFifg66hhHHGJ6ujTe",
|
|
85
|
+
".ZrpoCNSLKkGfh3355fffB4ejmj.",
|
|
86
|
+
"..1qpCY1PMLFfg3553hhhh.ihgRw",
|
|
87
|
+
".....ZZ2POJGdd653AdcgQQhfe..",
|
|
88
|
+
".....ZZPZPJdxjj6vcddcAAffT..",
|
|
89
|
+
".CC.YpCE2VhGkjmmfdcddghdT..A",
|
|
90
|
+
"....Xpp1ZVRVxhhfgddfffTT..Ab",
|
|
91
|
+
".....1Z2pC02VxxkxxgnmzA....6",
|
|
92
|
+
".......qrp2ewVPMxxmmavvv6..A",
|
|
93
|
+
"..UU...YXY..PPFkEkkvvaaavA..",
|
|
94
|
+
".UUCU......XXPMCkkvalanxavvN",
|
|
95
|
+
".UCyUNN..1Yu4PMP055nlalxBavN",
|
|
96
|
+
"..UU.NN.1uD2PwJ0MP33nlmyBuav",
|
|
97
|
+
"QyDy3y..uo2RRwFPPPP6nnBBavaa",
|
|
98
|
+
"DQQQDyy.tB2RVPPPSR4poBBaaB6N",
|
|
99
|
+
]
|
package/src/cli/logo/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { LOGO_KEYS, LOGO_PALETTE, LOGO_PIXELS } from "./pixels"
|
|
1
|
+
import { trueColor, useColor } from "../terminal/ui"
|
|
2
|
+
import { HALF_BLOCKS, KEYS, OCTANTS, PALETTE, PIXELS } from "./art"
|
|
4
3
|
|
|
5
4
|
type Rgb = [number, number, number]
|
|
6
5
|
type Pixel = Rgb | null
|
|
@@ -9,8 +8,6 @@ const rgb = (hex: string): Rgb => [1, 3, 5].map((i) => Number.parseInt(hex.slice
|
|
|
9
8
|
|
|
10
9
|
const toCube = (value: number) => (value < 48 ? 0 : value < 115 ? 1 : Math.min(5, Math.round((value - 35) / 40)))
|
|
11
10
|
|
|
12
|
-
const MISMATCH = 3 * 255 ** 2
|
|
13
|
-
|
|
14
11
|
function sgr(color: Rgb, layer: "fg" | "bg") {
|
|
15
12
|
const base = layer === "fg" ? 38 : 48
|
|
16
13
|
if (trueColor) return `${base};2;${color.join(";")}`
|
|
@@ -19,12 +16,12 @@ function sgr(color: Rgb, layer: "fg" | "bg") {
|
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
function pixel(key: string | undefined): Pixel {
|
|
22
|
-
if (!key || key ===
|
|
23
|
-
return rgb(
|
|
19
|
+
if (!key || key === ".") return null
|
|
20
|
+
return rgb(PALETTE[KEYS.indexOf(key)])
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
function distance(a: Pixel, b: Pixel) {
|
|
27
|
-
if (!a || !b) return a === b ? 0 :
|
|
24
|
+
if (!a || !b) return a === b ? 0 : 3 * 255 ** 2
|
|
28
25
|
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
|
|
29
26
|
}
|
|
30
27
|
|
|
@@ -57,26 +54,23 @@ function cell(block: Pixel[], glyphs: string[]) {
|
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
function supportsOctants() {
|
|
60
|
-
|
|
61
|
-
return OCTANT_TERMINALS.some((name) => terminal.includes(name))
|
|
57
|
+
return `${process.env.TERM_PROGRAM ?? ""} ${process.env.TERM ?? ""}`.toLowerCase().includes("ghostty")
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
function region(x: number, y: number, width: number, height: number): Pixel[] {
|
|
65
61
|
const block: Pixel[] = []
|
|
66
|
-
for (let dy = 0; dy < height; dy += 1) for (let dx = 0; dx < width; dx += 1) block.push(pixel(
|
|
62
|
+
for (let dy = 0; dy < height; dy += 1) for (let dx = 0; dx < width; dx += 1) block.push(pixel(PIXELS[y + dy]?.[x + dx]))
|
|
67
63
|
return block
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
function logoLines(): string[] {
|
|
71
67
|
if (!useColor) return []
|
|
72
68
|
const octants = supportsOctants()
|
|
73
69
|
const lines: string[] = []
|
|
74
|
-
for (let y = 0; y <
|
|
70
|
+
for (let y = 0; y < PIXELS.length; y += 4) {
|
|
75
71
|
let row = ""
|
|
76
|
-
for (let x = 0; x <
|
|
77
|
-
row += octants
|
|
78
|
-
? cell(region(x, y, 2, 4), OCTANTS)
|
|
79
|
-
: cell([dominant(region(x, y, 2, 2)), dominant(region(x, y + 2, 2, 2))], [...HALF_BLOCKS])
|
|
72
|
+
for (let x = 0; x < PIXELS[0].length; x += 2) {
|
|
73
|
+
row += octants ? cell(region(x, y, 2, 4), OCTANTS) : cell([dominant(region(x, y, 2, 2)), dominant(region(x, y + 2, 2, 2))], [...HALF_BLOCKS])
|
|
80
74
|
}
|
|
81
75
|
lines.push(row)
|
|
82
76
|
}
|
|
@@ -85,5 +79,5 @@ export function logoLines(): string[] {
|
|
|
85
79
|
|
|
86
80
|
export function withLogo(text: string[]): string[] {
|
|
87
81
|
const logo = logoLines()
|
|
88
|
-
return [...logo.map((row) => ` ${row}`), ...(logo.length ? [""] : []), ...text.map((entry) => ` ${entry}`)]
|
|
82
|
+
return [...logo.map((row) => ` ${row}`), ...(logo.length > 0 ? [""] : []), ...text.map((entry) => ` ${entry}`)]
|
|
89
83
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { VERSION } from "../version"
|
|
3
|
-
import { wantsHelp } from "./args"
|
|
4
3
|
import { commands } from "./commands"
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { wantsHelp } from "./terminal/args"
|
|
5
|
+
import { printCommandHelp, printHelp } from "./terminal/help"
|
|
6
|
+
import { closest, fail, line } from "./terminal/ui"
|
|
7
7
|
|
|
8
8
|
const [name, ...args] = process.argv.slice(2)
|
|
9
9
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export interface Settings {
|
|
5
|
+
env?: Record<string, string>
|
|
6
|
+
autoCompactEnabled?: boolean
|
|
7
|
+
[key: string]: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const BLOCKING_LIMIT_ENV = "CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE"
|
|
11
|
+
export const BLOCKING_LIMIT_OFF = "1000000000"
|
|
12
|
+
|
|
13
|
+
export const proxyUrl = (port: number) => `http://127.0.0.1:${port}`
|
|
14
|
+
|
|
15
|
+
export const pointsAtEunoe = (settings: Settings, port: number) => settings.env?.ANTHROPIC_BASE_URL === proxyUrl(port)
|
|
16
|
+
|
|
17
|
+
function withoutEnv(settings: Settings, name: string): Settings {
|
|
18
|
+
if (!settings.env || !(name in settings.env)) return settings
|
|
19
|
+
const { [name]: _, ...env } = settings.env
|
|
20
|
+
const next: Settings = { ...settings, env }
|
|
21
|
+
if (Object.keys(env).length === 0) delete next.env
|
|
22
|
+
return next
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function withCompactionOwner(settings: Settings, mode: string): Settings {
|
|
26
|
+
if (mode !== "off") return { ...settings, autoCompactEnabled: false, env: { ...settings.env, [BLOCKING_LIMIT_ENV]: BLOCKING_LIMIT_OFF } }
|
|
27
|
+
const { autoCompactEnabled: _, ...rest } = settings
|
|
28
|
+
return rest.env?.[BLOCKING_LIMIT_ENV] === BLOCKING_LIMIT_OFF ? withoutEnv(rest, BLOCKING_LIMIT_ENV) : rest
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function withEunoe(settings: Settings, port: number, mode: string): Settings {
|
|
32
|
+
return withCompactionOwner({ ...settings, env: { ...settings.env, ANTHROPIC_BASE_URL: proxyUrl(port) } }, mode)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function withoutEunoe(settings: Settings, port: number): Settings {
|
|
36
|
+
return withCompactionOwner(pointsAtEunoe(settings, port) ? withoutEnv(settings, "ANTHROPIC_BASE_URL") : settings, "off")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readSettings(dir: string): Settings {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(path.join(dir, "settings.json"), "utf8"))
|
|
42
|
+
} catch {
|
|
43
|
+
return {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function editSettings(dirs: string[], edit: (settings: Settings) => Settings): string[] {
|
|
48
|
+
const touched: string[] = []
|
|
49
|
+
for (const dir of dirs) {
|
|
50
|
+
if (!existsSync(dir)) continue
|
|
51
|
+
const file = path.join(dir, "settings.json")
|
|
52
|
+
const current: Settings = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {}
|
|
53
|
+
const next = edit(current)
|
|
54
|
+
if (JSON.stringify(next) === JSON.stringify(current)) continue
|
|
55
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`)
|
|
56
|
+
touched.push(file)
|
|
57
|
+
}
|
|
58
|
+
return touched
|
|
59
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { type Config, claudeConfigDirs } from "../../config"
|
|
4
|
+
import { paths } from "../../config/paths"
|
|
5
|
+
import { HEALTH_PATH, type Health } from "../../proxy/server"
|
|
6
|
+
import { parseJsonl } from "../../transcript/parse"
|
|
7
|
+
import { pointsAtEunoe, proxyUrl, readSettings } from "./claude-settings"
|
|
8
|
+
|
|
9
|
+
export interface RequestRecord {
|
|
10
|
+
ts: string
|
|
11
|
+
action?: string
|
|
12
|
+
thread?: string | null
|
|
13
|
+
input?: number
|
|
14
|
+
charsIn?: number
|
|
15
|
+
charsOut?: number
|
|
16
|
+
charsFixed?: number
|
|
17
|
+
cacheRead?: number
|
|
18
|
+
cacheWrite?: number
|
|
19
|
+
uncached?: number
|
|
20
|
+
compacted?: boolean
|
|
21
|
+
turns?: number
|
|
22
|
+
reranked?: string
|
|
23
|
+
rerankError?: string
|
|
24
|
+
overflow?: number
|
|
25
|
+
fit?: string
|
|
26
|
+
[key: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const RECENT_REQUESTS = 5_000
|
|
30
|
+
|
|
31
|
+
export async function probe(port: number): Promise<Health | null> {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`${proxyUrl(port)}${HEALTH_PATH}`, { signal: AbortSignal.timeout(800) })
|
|
34
|
+
if (!response.ok) return null
|
|
35
|
+
return (await response.json()) as Health
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function newestSource(dir: string): number {
|
|
42
|
+
let newest = 0
|
|
43
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const file = path.join(dir, entry.name)
|
|
45
|
+
if (entry.isDirectory()) newest = Math.max(newest, newestSource(file))
|
|
46
|
+
else if (entry.name.endsWith(".ts")) newest = Math.max(newest, statSync(file).mtimeMs)
|
|
47
|
+
}
|
|
48
|
+
return newest
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runsStaleCode(health: Health | null): boolean {
|
|
52
|
+
if (!health?.startedAt) return health !== null
|
|
53
|
+
try {
|
|
54
|
+
return newestSource(path.join(import.meta.dir, "..")) > health.startedAt
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function routedDirs(config: Config) {
|
|
61
|
+
return claudeConfigDirs(config).filter((dir) => pointsAtEunoe(readSettings(dir), config.port))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function recentRequests(limit = RECENT_REQUESTS): RequestRecord[] {
|
|
65
|
+
if (!existsSync(paths.requests)) return []
|
|
66
|
+
return parseJsonl<RequestRecord>(readFileSync(paths.requests, "utf8")).slice(-limit)
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export interface Agent {
|
|
5
|
+
label: string
|
|
6
|
+
plist: string
|
|
7
|
+
interval?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const supported = process.platform === "darwin"
|
|
11
|
+
|
|
12
|
+
export function isInstalled(agent: Agent) {
|
|
13
|
+
return existsSync(agent.plist)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function plistFor(agent: Agent, program: string[], logFile: string) {
|
|
17
|
+
const args = program.map((arg) => `<string>${arg}</string>`).join("")
|
|
18
|
+
const schedule = agent.interval ? `<key>StartInterval</key><integer>${agent.interval}</integer>` : `<key>KeepAlive</key><true/>\n <key>ThrottleInterval</key><integer>1</integer>`
|
|
19
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
21
|
+
<plist version="1.0"><dict>
|
|
22
|
+
<key>Label</key><string>${agent.label}</string>
|
|
23
|
+
<key>ProgramArguments</key><array>${args}</array>
|
|
24
|
+
<key>RunAtLoad</key><true/>
|
|
25
|
+
${schedule}
|
|
26
|
+
<key>StandardOutPath</key><string>${logFile}</string>
|
|
27
|
+
<key>StandardErrorPath</key><string>${logFile}</string>
|
|
28
|
+
</dict></plist>
|
|
29
|
+
`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function installAgent(agent: Agent, program: string[], logFile: string) {
|
|
33
|
+
mkdirSync(path.dirname(agent.plist), { recursive: true })
|
|
34
|
+
const plist = plistFor(agent, program, logFile)
|
|
35
|
+
if (existsSync(agent.plist) && readFileSync(agent.plist, "utf8") === plist) {
|
|
36
|
+
Bun.spawnSync(["launchctl", "load", agent.plist])
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
writeFileSync(agent.plist, plist)
|
|
40
|
+
Bun.spawnSync(["launchctl", "unload", agent.plist])
|
|
41
|
+
Bun.spawnSync(["launchctl", "load", agent.plist])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function restartAgent(agent: Agent) {
|
|
45
|
+
Bun.spawnSync(["launchctl", "kickstart", "-k", `gui/${process.getuid?.() ?? ""}/${agent.label}`])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function removeAgent(agent: Agent) {
|
|
49
|
+
if (!existsSync(agent.plist)) return false
|
|
50
|
+
Bun.spawnSync(["launchctl", "unload", agent.plist])
|
|
51
|
+
unlinkSync(agent.plist)
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { Config } from "../../config"
|
|
4
|
+
import { paths } from "../../config/paths"
|
|
5
|
+
import type { Health } from "../../proxy/server"
|
|
6
|
+
import { c, line, ok, warn } from "../terminal/ui"
|
|
7
|
+
import { proxyUrl } from "./claude-settings"
|
|
8
|
+
import { probe, runsStaleCode } from "./inspect"
|
|
9
|
+
import * as launchd from "./launchd"
|
|
10
|
+
|
|
11
|
+
const agentFor = (label: string, interval?: number): launchd.Agent => ({ label, plist: path.join(homedir(), "Library", "LaunchAgents", `${label}.plist`), interval })
|
|
12
|
+
|
|
13
|
+
export const PROXY_AGENT = agentFor("com.chroxify.eunoe")
|
|
14
|
+
const WATCH_AGENT = agentFor("com.chroxify.eunoe.watch", 10)
|
|
15
|
+
|
|
16
|
+
const cli = () => path.resolve(process.argv[1] ?? "")
|
|
17
|
+
|
|
18
|
+
async function waitFor(port: number, ready: (health: Health | null) => boolean) {
|
|
19
|
+
const deadline = Date.now() + 3_000
|
|
20
|
+
while (Date.now() < deadline) {
|
|
21
|
+
if (ready(await probe(port))) return true
|
|
22
|
+
await Bun.sleep(150)
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function startService(config: Config) {
|
|
28
|
+
if (!launchd.supported) {
|
|
29
|
+
if (await probe(config.port)) return true
|
|
30
|
+
warn("No launchd here, so keep the proxy running yourself:")
|
|
31
|
+
line(` ${c.cyan("eunoe serve")}`)
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
launchd.installAgent(PROXY_AGENT, [process.execPath, cli(), "serve"], paths.proxyLog)
|
|
35
|
+
launchd.installAgent(WATCH_AGENT, [process.execPath, cli(), "watch"], paths.proxyLog)
|
|
36
|
+
const running = await probe(config.port)
|
|
37
|
+
if (running && runsStaleCode(running)) {
|
|
38
|
+
launchd.restartAgent(PROXY_AGENT)
|
|
39
|
+
await waitFor(config.port, (health) => health !== null && health.pid !== running.pid)
|
|
40
|
+
}
|
|
41
|
+
if (await waitFor(config.port, (health) => health !== null)) {
|
|
42
|
+
ok(`Proxy running in the background on ${c.cyan(proxyUrl(config.port))}`)
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
warn(`The proxy didn't answer on ${proxyUrl(config.port)}. Check ${paths.proxyLog}.`)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function removeService() {
|
|
50
|
+
const removed = launchd.removeAgent(PROXY_AGENT)
|
|
51
|
+
launchd.removeAgent(WATCH_AGENT)
|
|
52
|
+
return removed
|
|
53
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { RequestRecord } from "./types"
|
|
1
|
+
import type { RequestRecord } from "./inspect"
|
|
3
2
|
|
|
4
3
|
export interface SessionStats {
|
|
5
4
|
context: number
|
|
5
|
+
fixed: number | null
|
|
6
6
|
plain: number | null
|
|
7
7
|
turns: number | null
|
|
8
8
|
requests: number
|
|
@@ -21,13 +21,17 @@ export interface Totals {
|
|
|
21
21
|
saved: number
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const sessionOf = (record: RequestRecord): string | null => (typeof record.thread === "string" ? record.thread.replace(/-[0-9a-f]{24}$/, "") : null)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
const inputOf = (record: RequestRecord) => record.input ?? 0
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function scaled(record: RequestRecord, chars: number | undefined): number | null {
|
|
29
|
+
const input = inputOf(record)
|
|
30
|
+
const charsOut = record.charsOut ?? 0
|
|
31
|
+
return input > 0 && charsOut > 0 && chars && chars > 0 ? Math.round((input * chars) / charsOut) : null
|
|
32
|
+
}
|
|
29
33
|
|
|
30
|
-
export function withinWindow(records: RequestRecord[], now = Date.now(), window =
|
|
34
|
+
export function withinWindow(records: RequestRecord[], now = Date.now(), window = 24 * 3_600_000): RequestRecord[] {
|
|
31
35
|
return records.filter((record) => now - Date.parse(record.ts) <= window)
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -35,12 +39,13 @@ export function perSession(records: RequestRecord[]): Map<string, SessionStats>
|
|
|
35
39
|
const stats = new Map<string, SessionStats>()
|
|
36
40
|
for (const record of records) {
|
|
37
41
|
const id = sessionOf(record)
|
|
38
|
-
if (!id ||
|
|
42
|
+
if (!id || inputOf(record) <= 0) continue
|
|
39
43
|
const previous = stats.get(id)
|
|
40
44
|
stats.set(id, {
|
|
41
|
-
context: record
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
context: inputOf(record),
|
|
46
|
+
fixed: scaled(record, record.charsFixed),
|
|
47
|
+
plain: scaled(record, record.charsIn),
|
|
48
|
+
turns: record.turns ?? previous?.turns ?? null,
|
|
44
49
|
requests: (previous?.requests ?? 0) + 1,
|
|
45
50
|
folds: (previous?.folds ?? 0) + (record.compacted ? 1 : 0),
|
|
46
51
|
lastSeen: record.ts,
|
|
@@ -52,7 +57,7 @@ export function perSession(records: RequestRecord[]): Map<string, SessionStats>
|
|
|
52
57
|
export function totals(records: RequestRecord[]): Totals {
|
|
53
58
|
const out: Totals = { requests: 0, cacheRead: 0, cacheWrite: 0, uncached: 0, folds: 0, lastFold: null, sent: 0, saved: 0 }
|
|
54
59
|
for (const record of records) {
|
|
55
|
-
if (
|
|
60
|
+
if (inputOf(record) <= 0) continue
|
|
56
61
|
out.requests += 1
|
|
57
62
|
out.cacheRead += record.cacheRead ?? 0
|
|
58
63
|
out.cacheWrite += record.cacheWrite ?? 0
|
|
@@ -61,13 +66,16 @@ export function totals(records: RequestRecord[]): Totals {
|
|
|
61
66
|
out.folds += 1
|
|
62
67
|
out.lastFold = record.ts
|
|
63
68
|
}
|
|
64
|
-
const plain =
|
|
69
|
+
const plain = scaled(record, record.charsIn)
|
|
65
70
|
if (plain !== null) {
|
|
66
|
-
out.sent += record
|
|
67
|
-
out.saved += plain - record
|
|
71
|
+
out.sent += inputOf(record)
|
|
72
|
+
out.saved += plain - inputOf(record)
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
return out
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
export
|
|
78
|
+
export function savedShare(context: number, plain: number | null): number | null {
|
|
79
|
+
if (!plain) return null
|
|
80
|
+
return plain > context ? 1 - context / plain : 0
|
|
81
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { type Config, claudeConfigDirs, compactionOwner } from "../../config"
|
|
3
|
+
import { paths } from "../../config/paths"
|
|
4
|
+
import { editSettings, withEunoe, withoutEunoe } from "./claude-settings"
|
|
5
|
+
import { probe, routedDirs } from "./inspect"
|
|
6
|
+
|
|
7
|
+
interface Watch {
|
|
8
|
+
misses: number
|
|
9
|
+
paused: string[]
|
|
10
|
+
pausedAt?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface WatchStep {
|
|
14
|
+
watch: Watch
|
|
15
|
+
pause: string[]
|
|
16
|
+
resume: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const WATCH_MISSES = 2
|
|
20
|
+
|
|
21
|
+
const idle = (): Watch => ({ misses: 0, paused: [] })
|
|
22
|
+
|
|
23
|
+
export function readWatch(): Watch {
|
|
24
|
+
try {
|
|
25
|
+
return { ...idle(), ...JSON.parse(readFileSync(paths.watch, "utf8")) }
|
|
26
|
+
} catch {
|
|
27
|
+
return idle()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveWatch(watch: Watch) {
|
|
32
|
+
if (watch.misses === 0 && watch.paused.length === 0) {
|
|
33
|
+
if (existsSync(paths.watch)) unlinkSync(paths.watch)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
writeFileSync(paths.watch, `${JSON.stringify(watch, null, 2)}\n`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearWatch() {
|
|
40
|
+
saveWatch(idle())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function nextWatch(watch: Watch, alive: boolean, routed: string[], now: string): WatchStep {
|
|
44
|
+
if (alive) return { watch: idle(), pause: [], resume: watch.paused }
|
|
45
|
+
if (routed.length === 0) return { watch: { ...watch, misses: 0 }, pause: [], resume: [] }
|
|
46
|
+
const misses = watch.misses + 1
|
|
47
|
+
if (misses < WATCH_MISSES) return { watch: { ...watch, misses }, pause: [], resume: [] }
|
|
48
|
+
return { watch: { misses: 0, paused: [...new Set([...watch.paused, ...routed])], pausedAt: now }, pause: routed, resume: [] }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runWatch(config: Config): Promise<WatchStep> {
|
|
52
|
+
const alive = (await probe(config.port)) !== null
|
|
53
|
+
const step = nextWatch(readWatch(), alive, routedDirs(config), new Date().toISOString())
|
|
54
|
+
if (step.pause.length > 0) editSettings(step.pause, (settings) => withoutEunoe(settings, config.port))
|
|
55
|
+
const managed = claudeConfigDirs(config)
|
|
56
|
+
const resumed = step.resume.filter((dir) => managed.includes(dir))
|
|
57
|
+
if (resumed.length > 0) editSettings(resumed, (settings) => withEunoe(settings, config.port, compactionOwner(config)))
|
|
58
|
+
saveWatch(step.watch)
|
|
59
|
+
return step
|
|
60
|
+
}
|