@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
|
@@ -1,82 +1,125 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
-
import {
|
|
3
|
-
import { paths
|
|
4
|
-
import { rerankKey } from "../../context/rerank"
|
|
2
|
+
import { type Config, modeFor, overrides, readConfig } from "../../config"
|
|
3
|
+
import { paths } from "../../config/paths"
|
|
4
|
+
import { rerankKey } from "../../context/recall/rerank"
|
|
5
|
+
import type { Health } from "../../proxy/server"
|
|
5
6
|
import { seenSessions } from "../../proxy/sessions"
|
|
6
7
|
import { VERSION } from "../../version"
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { probe, recentRequests, routedDirs } from "../inspect"
|
|
10
|
-
import * as launchd from "../launchd"
|
|
8
|
+
import type { Command } from "../command"
|
|
9
|
+
import { BLOCKING_LIMIT_ENV, proxyUrl, readSettings } from "../proxy/claude-settings"
|
|
10
|
+
import { probe, RECENT_REQUESTS, type RequestRecord, recentRequests, routedDirs, runsStaleCode } from "../proxy/inspect"
|
|
11
|
+
import * as launchd from "../proxy/launchd"
|
|
12
|
+
import { PROXY_AGENT } from "../proxy/service"
|
|
13
|
+
import { perSession, type SessionStats, savedShare, type Totals, totals, withinWindow } from "../proxy/stats"
|
|
14
|
+
import { readWatch } from "../proxy/watch"
|
|
11
15
|
import { shortId } from "../sessions"
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import { ago, c, line, percent, rows, table, tilde, tokens, warn } from "../ui"
|
|
16
|
+
import { ago, c, line, percent, plural, rows, table, tilde, tokens, warn } from "../terminal/ui"
|
|
17
|
+
import { MODE_DESCRIPTIONS } from "./mode"
|
|
15
18
|
|
|
16
19
|
const dot = (on: boolean) => (on ? c.green("●") : c.red("○"))
|
|
17
20
|
|
|
21
|
+
const settingsFiles = (dirs: string[]) => dirs.map((dir) => tilde(path.join(dir, "settings.json"))).join(", ")
|
|
22
|
+
|
|
23
|
+
function summary(title: string, sum: Totals) {
|
|
24
|
+
const hit = sum.cacheRead + sum.cacheWrite + sum.uncached
|
|
25
|
+
const folds = sum.folds && sum.lastFold ? ` ${c.dim(`${plural(sum.folds, "fold")}, last ${ago(sum.lastFold)}`)}` : ""
|
|
26
|
+
const hitRate = hit ? c.dim(` (${percent(sum.cacheRead / hit)} hit)`) : ""
|
|
27
|
+
const saved = sum.saved > 0 ? `${c.bold(c.green(tokens(sum.saved)))} input tokens not sent ${c.dim(`(${percent(sum.saved / (sum.sent + sum.saved))} less than plain Claude Code)`)}` : c.dim("no data yet")
|
|
28
|
+
line()
|
|
29
|
+
line(` ${title}`)
|
|
30
|
+
rows(
|
|
31
|
+
[
|
|
32
|
+
["Requests", `${c.bold(sum.requests.toLocaleString())}${folds}`],
|
|
33
|
+
["Cache", `${c.bold(tokens(sum.cacheRead))} read ${c.dim("·")} ${tokens(sum.cacheWrite)} written ${c.dim("·")} ${tokens(sum.uncached)} uncached${hitRate}`],
|
|
34
|
+
["Saved", saved],
|
|
35
|
+
],
|
|
36
|
+
4,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recallLine(config: Config, recent: RequestRecord[]) {
|
|
41
|
+
if (config.rerank === "off") return "bm25"
|
|
42
|
+
if (!rerankKey()) return `bm25 ${c.dim(`(${config.rerank} needs a key in ${tilde(paths.typesafeKey)} or TYPESAFE_API_KEY)`)}`
|
|
43
|
+
const last = recent.findLast((r) => r.reranked || r.rerankError)
|
|
44
|
+
if (last?.rerankError) return `bm25 ${c.yellow(`(${config.rerank} failing: ${last.rerankError.replace(/^[^:]+: /, "")}, last ${ago(last.ts)})`)}`
|
|
45
|
+
return `bm25 ${c.dim("→")} ${config.rerank}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sessionRow(config: Config, stats: Map<string, SessionStats>, [id, session]: ReturnType<typeof seenSessions>[number]) {
|
|
49
|
+
const active = modeFor(config, id)
|
|
50
|
+
const own = Object.entries(overrides(config.sessions[id]))
|
|
51
|
+
.map(([name, value]) => `${name} ${value}`)
|
|
52
|
+
.join(" · ")
|
|
53
|
+
const stat = stats.get(id)
|
|
54
|
+
const managed = active !== "off"
|
|
55
|
+
const saved = stat ? savedShare(stat.context, stat.plain) : null
|
|
56
|
+
return [
|
|
57
|
+
c.cyan(shortId(id)),
|
|
58
|
+
managed ? c.green(active) : c.dim("off"),
|
|
59
|
+
session.cwd ? tilde(session.cwd) : c.dim("–"),
|
|
60
|
+
stat?.turns ? c.dim(`${stat.turns} turns`) : "",
|
|
61
|
+
stat && managed ? `${c.bold(tokens(stat.context))} ctx${stat.fixed ? c.dim(` (${tokens(stat.fixed)} fixed)`) : ""}` : "",
|
|
62
|
+
saved !== null && managed ? (saved > 0 ? c.green(`−${percent(saved)}`) : c.dim("±0")) : "",
|
|
63
|
+
c.dim(ago(session.lastSeen)),
|
|
64
|
+
own ? c.dim(own) : "",
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sessionsTable(config: Config, stats: Map<string, SessionStats>) {
|
|
69
|
+
const sessions = seenSessions().slice(0, 5)
|
|
70
|
+
if (sessions.length === 0) return
|
|
71
|
+
line()
|
|
72
|
+
line(` ${c.dim("Sessions")}`)
|
|
73
|
+
table(sessions.map((entry) => sessionRow(config, stats, entry)))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function warnings(config: Config, health: Health | null, routed: string[], recent: RequestRecord[]) {
|
|
77
|
+
const notes: string[] = []
|
|
78
|
+
const watch = readWatch()
|
|
79
|
+
if (watch.paused.length > 0)
|
|
80
|
+
notes.push(`The proxy stopped answering ${watch.pausedAt ? ago(watch.pausedAt) : ""}, so Claude Code was sent straight to the API. Routing comes back by itself once the proxy does; ${c.bold("eunoe start")} forces it.`)
|
|
81
|
+
if (health && health.mode !== config.mode) notes.push(`The running proxy reports mode ${health.mode}; it switches on its next request.`)
|
|
82
|
+
if (health && runsStaleCode(health)) notes.push(`The proxy is running older code than what's installed. ${c.bold("eunoe start")} restarts it.`)
|
|
83
|
+
const unguarded = config.mode !== "off" ? routed.filter((dir) => readSettings(dir).env?.[BLOCKING_LIMIT_ENV] === undefined) : []
|
|
84
|
+
if (unguarded.length > 0) notes.push(`Claude Code can still refuse to send near a full window in ${settingsFiles(unguarded)}. ${c.bold("eunoe start")} fixes it.`)
|
|
85
|
+
const missed = recent.filter((r) => r.overflow || r.fit === "over")
|
|
86
|
+
const lastMissed = missed.at(-1)
|
|
87
|
+
if (lastMissed) notes.push(`${missed.length} request${missed.length === 1 ? "" : "s"} didn't fit the window on the first try, last ${ago(lastMissed.ts)}. Details in ${tilde(paths.requests)}.`)
|
|
88
|
+
for (const text of notes) {
|
|
89
|
+
line()
|
|
90
|
+
warn(text)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
18
94
|
export const status: Command = {
|
|
19
95
|
summary: "Show what eunoe is doing and saving",
|
|
20
96
|
usage: "eunoe status",
|
|
21
97
|
async run() {
|
|
22
|
-
const config =
|
|
98
|
+
const config = readConfig()
|
|
23
99
|
const health = await probe(config.port)
|
|
24
100
|
const routed = routedDirs(config)
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
101
|
+
const all = recentRequests(Number.POSITIVE_INFINITY)
|
|
102
|
+
const recent = all.slice(-RECENT_REQUESTS)
|
|
103
|
+
const sum = totals(withinWindow(all))
|
|
104
|
+
const allTime = totals(all)
|
|
29
105
|
|
|
30
106
|
line()
|
|
31
107
|
line(` ${c.bold("eunoe")} ${c.dim(VERSION)}`)
|
|
32
108
|
line()
|
|
33
|
-
|
|
34
|
-
const recall = config.rerank === RERANK_OFF ? "bm25" : rerankKey() ? `bm25 ${c.dim("→")} ${config.rerank}` : `bm25 ${c.dim(`(${config.rerank} needs a key in ${tilde(paths.typesafeKey)} or TYPESAFE_API_KEY)`)}`
|
|
35
109
|
rows([
|
|
36
|
-
["Proxy", health ? `${dot(true)} running on ${c.cyan(proxyUrl(config.port))}${launchd.isInstalled() ? c.dim(" launchd") : ""}` : `${dot(false)} not running ${c.dim("(eunoe start)")}`],
|
|
37
|
-
["Claude Code", routed.length ? `${dot(true)} routed ${c.dim(routed
|
|
110
|
+
["Proxy", health ? `${dot(true)} running on ${c.cyan(proxyUrl(config.port))}${launchd.isInstalled(PROXY_AGENT) ? c.dim(" launchd") : ""}` : `${dot(false)} not running ${c.dim("(eunoe start)")}`],
|
|
111
|
+
["Claude Code", routed.length > 0 ? `${dot(true)} routed ${c.dim(settingsFiles(routed))}` : `${c.dim("○")} not routed ${c.dim("(eunoe start)")}`],
|
|
38
112
|
["Mode", `${c.bold(config.mode)} ${c.dim(MODE_DESCRIPTIONS[config.mode])}`],
|
|
39
113
|
["Sessions", config.scope === "all" ? "all" : `only the ones you started it for ${c.dim("(eunoe start --session <id>)")}`],
|
|
40
|
-
["Recall",
|
|
114
|
+
["Recall", recallLine(config, recent)],
|
|
41
115
|
])
|
|
42
116
|
|
|
43
|
-
|
|
44
|
-
if (sessions.length) {
|
|
45
|
-
line()
|
|
46
|
-
line(` ${c.dim("Sessions")}`)
|
|
47
|
-
table(sessions.map(([id, session]) => {
|
|
48
|
-
const active = modeFor(config, id)
|
|
49
|
-
const own = Object.entries(overrides(config.sessions[id])).map(([name, value]) => `${name} ${value}`).join(" · ")
|
|
50
|
-
const stat = stats.get(id)
|
|
51
|
-
const saved = stat ? savedShare(stat.context, stat.plain) : null
|
|
52
|
-
return [
|
|
53
|
-
c.cyan(shortId(id)),
|
|
54
|
-
active === "off" ? c.dim("off") : c.green(active),
|
|
55
|
-
session.cwd ? tilde(session.cwd) : c.dim("–"),
|
|
56
|
-
stat?.turns ? c.dim(`${stat.turns} turns`) : "",
|
|
57
|
-
stat && active !== "off" ? `${c.bold(tokens(stat.context))} ctx` : "",
|
|
58
|
-
saved !== null && active !== "off" ? (saved > 0 ? c.green(`−${percent(saved)}`) : c.dim("±0")) : "",
|
|
59
|
-
c.dim(ago(session.lastSeen)),
|
|
60
|
-
own ? c.dim(own) : "",
|
|
61
|
-
]
|
|
62
|
-
}))
|
|
63
|
-
}
|
|
117
|
+
sessionsTable(config, perSession(all))
|
|
64
118
|
|
|
65
|
-
if (sum.requests)
|
|
66
|
-
|
|
67
|
-
line()
|
|
68
|
-
line(` ${c.dim("Last 24h")}`)
|
|
69
|
-
rows([
|
|
70
|
-
["Requests", `${c.bold(String(sum.requests))}${sum.folds ? ` ${c.dim(`${sum.folds} fold${sum.folds === 1 ? "" : "s"}, last ${ago(sum.lastFold!)}`)}` : ""}`],
|
|
71
|
-
["Cache", `${c.bold(tokens(sum.cacheRead))} read ${c.dim("·")} ${tokens(sum.cacheWrite)} written ${c.dim("·")} ${tokens(sum.uncached)} uncached${hit ? c.dim(` (${percent(sum.cacheRead / hit)} hit)`) : ""}`],
|
|
72
|
-
["Saved", sum.saved > 0 ? `${c.bold(c.green(tokens(sum.saved)))} input tokens not sent ${c.dim(`(${percent(sum.saved / (sum.sent + sum.saved))} less than plain Claude Code)`)}` : c.dim("no data yet")],
|
|
73
|
-
], 4)
|
|
74
|
-
}
|
|
119
|
+
if (sum.requests) summary(c.dim("Last 24h"), sum)
|
|
120
|
+
if (allTime.requests > sum.requests) summary(`${c.dim("All time")} ${c.dim(`since ${new Date(all[0].ts).toLocaleDateString()}`)}`, allTime)
|
|
75
121
|
|
|
76
|
-
|
|
77
|
-
line()
|
|
78
|
-
warn(`The running proxy reports mode ${health.mode}; it switches on its next request.`)
|
|
79
|
-
}
|
|
122
|
+
warnings(config, health, routed, recent)
|
|
80
123
|
line()
|
|
81
124
|
},
|
|
82
125
|
}
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { claudeConfigDirs,
|
|
2
|
-
import {
|
|
1
|
+
import { claudeConfigDirs, readConfig, saveConfig } from "../../config"
|
|
2
|
+
import type { Command } from "../command"
|
|
3
|
+
import { editSettings, withoutEunoe } from "../proxy/claude-settings"
|
|
4
|
+
import { clearWatch } from "../proxy/watch"
|
|
3
5
|
import { sessionArgs, shortId } from "../sessions"
|
|
4
|
-
import
|
|
5
|
-
import { line, listFiles, note, ok } from "../ui"
|
|
6
|
+
import { line, listFiles, note, ok } from "../terminal/ui"
|
|
6
7
|
|
|
7
8
|
export const stop: Command = {
|
|
8
9
|
summary: "Send new sessions straight to the API again",
|
|
@@ -16,9 +17,9 @@ export const stop: Command = {
|
|
|
16
17
|
],
|
|
17
18
|
options: [["--session <id>", "Stop managing just this session (repeatable; a unique prefix is enough)"]],
|
|
18
19
|
run(args) {
|
|
19
|
-
const config =
|
|
20
|
+
const config = readConfig()
|
|
20
21
|
const ids = sessionArgs(args)
|
|
21
|
-
if (ids.length) {
|
|
22
|
+
if (ids.length > 0) {
|
|
22
23
|
const sessions = { ...config.sessions }
|
|
23
24
|
for (const id of ids) {
|
|
24
25
|
if (config.scope === "all") sessions[id] = { ...sessions[id], mode: "off" }
|
|
@@ -29,6 +30,7 @@ export const stop: Command = {
|
|
|
29
30
|
return
|
|
30
31
|
}
|
|
31
32
|
const touched = editSettings(claudeConfigDirs(config), (settings) => withoutEunoe(settings, config.port))
|
|
33
|
+
clearWatch()
|
|
32
34
|
line()
|
|
33
35
|
ok("Claude Code no longer routed through eunoe")
|
|
34
36
|
listFiles(touched)
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import { claudeConfigDirs,
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
import { claudeConfigDirs, readConfig } from "../../config"
|
|
2
|
+
import type { Command } from "../command"
|
|
3
|
+
import { editSettings, withoutEunoe } from "../proxy/claude-settings"
|
|
4
|
+
import { removeService } from "../proxy/service"
|
|
5
|
+
import { clearWatch } from "../proxy/watch"
|
|
6
|
+
import { c, line, listFiles, note, ok, warn } from "../terminal/ui"
|
|
6
7
|
|
|
7
8
|
export const uninstall: Command = {
|
|
8
9
|
summary: "Stop eunoe and remove the background proxy",
|
|
9
10
|
usage: "eunoe uninstall",
|
|
10
|
-
details: [
|
|
11
|
-
"Everything `stop` does, then shuts the proxy down and removes its background service.",
|
|
12
|
-
"Sessions still running through eunoe lose their connection, so restart them first.",
|
|
13
|
-
"Your config and transcripts in ~/.eunoe are kept.",
|
|
14
|
-
],
|
|
11
|
+
details: ["Everything `stop` does, then shuts the proxy down and removes its background service.", "Sessions still running through eunoe lose their connection, so restart them first.", "Your config and transcripts in ~/.eunoe are kept."],
|
|
15
12
|
run() {
|
|
16
|
-
const config =
|
|
13
|
+
const config = readConfig()
|
|
17
14
|
const touched = editSettings(claudeConfigDirs(config), (settings) => withoutEunoe(settings, config.port))
|
|
15
|
+
clearWatch()
|
|
18
16
|
line()
|
|
19
17
|
ok("Claude Code no longer routed through eunoe")
|
|
20
18
|
listFiles(touched)
|
|
21
|
-
if (
|
|
19
|
+
if (removeService()) ok("Background proxy stopped and removed")
|
|
22
20
|
else note("No background proxy was installed.")
|
|
23
21
|
warn(`Restart any Claude Code session that was still using eunoe.`)
|
|
24
22
|
line(` ${c.dim("Data kept in")} ~/.eunoe`)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readConfig } from "../../config"
|
|
2
|
+
import type { Command } from "../command"
|
|
3
|
+
import { runWatch } from "../proxy/watch"
|
|
4
|
+
import { line } from "../terminal/ui"
|
|
5
|
+
|
|
6
|
+
export const watch: Command = {
|
|
7
|
+
summary: "Check the proxy once and unroute Claude Code while it is down",
|
|
8
|
+
usage: "eunoe watch",
|
|
9
|
+
details: ["`eunoe start` schedules this in the background. When the proxy stops answering, Claude Code goes straight to the API; when it is back, routing returns."],
|
|
10
|
+
hidden: true,
|
|
11
|
+
async run() {
|
|
12
|
+
const step = await runWatch(readConfig())
|
|
13
|
+
for (const dir of step.pause) line(`eunoe watch proxy down, routing removed from ${dir}`)
|
|
14
|
+
for (const dir of step.resume) line(`eunoe watch proxy back, routing restored in ${dir}`)
|
|
15
|
+
},
|
|
16
|
+
}
|
|
@@ -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,69 @@
|
|
|
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
|
+
const CLAUDE_CLEANUP_DAYS = 30
|
|
11
|
+
|
|
12
|
+
export const BLOCKING_LIMIT_ENV = "CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE"
|
|
13
|
+
export const BLOCKING_LIMIT_OFF = "1000000000"
|
|
14
|
+
|
|
15
|
+
export const proxyUrl = (port: number) => `http://127.0.0.1:${port}`
|
|
16
|
+
|
|
17
|
+
export const pointsAtEunoe = (settings: Settings, port: number) => settings.env?.ANTHROPIC_BASE_URL === proxyUrl(port)
|
|
18
|
+
|
|
19
|
+
function withoutEnv(settings: Settings, name: string): Settings {
|
|
20
|
+
if (!settings.env || !(name in settings.env)) return settings
|
|
21
|
+
const { [name]: _, ...env } = settings.env
|
|
22
|
+
const next: Settings = { ...settings, env }
|
|
23
|
+
if (Object.keys(env).length === 0) delete next.env
|
|
24
|
+
return next
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function withCompactionOwner(settings: Settings, mode: string): Settings {
|
|
28
|
+
if (mode !== "off") return { ...settings, autoCompactEnabled: false, env: { ...settings.env, [BLOCKING_LIMIT_ENV]: BLOCKING_LIMIT_OFF } }
|
|
29
|
+
const { autoCompactEnabled: _, ...rest } = settings
|
|
30
|
+
return rest.env?.[BLOCKING_LIMIT_ENV] === BLOCKING_LIMIT_OFF ? withoutEnv(rest, BLOCKING_LIMIT_ENV) : rest
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function withEunoe(settings: Settings, port: number, mode: string): Settings {
|
|
34
|
+
return withCompactionOwner({ ...settings, env: { ...settings.env, ANTHROPIC_BASE_URL: proxyUrl(port) } }, mode)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function withoutEunoe(settings: Settings, port: number): Settings {
|
|
38
|
+
return withCompactionOwner(pointsAtEunoe(settings, port) ? withoutEnv(settings, "ANTHROPIC_BASE_URL") : settings, "off")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readSettings(dir: string): Settings {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(path.join(dir, "settings.json"), "utf8"))
|
|
44
|
+
} catch {
|
|
45
|
+
return {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function cleanupDays(configDirs: string[]): number {
|
|
50
|
+
for (const dir of configDirs) {
|
|
51
|
+
const days = readSettings(dir).cleanupPeriodDays
|
|
52
|
+
if (typeof days === "number") return days
|
|
53
|
+
}
|
|
54
|
+
return CLAUDE_CLEANUP_DAYS
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function editSettings(dirs: string[], edit: (settings: Settings) => Settings): string[] {
|
|
58
|
+
const touched: string[] = []
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
if (!existsSync(dir)) continue
|
|
61
|
+
const file = path.join(dir, "settings.json")
|
|
62
|
+
const current: Settings = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {}
|
|
63
|
+
const next = edit(current)
|
|
64
|
+
if (JSON.stringify(next) === JSON.stringify(current)) continue
|
|
65
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`)
|
|
66
|
+
touched.push(file)
|
|
67
|
+
}
|
|
68
|
+
return touched
|
|
69
|
+
}
|
|
@@ -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
|
+
}
|