@compaction/cli 0.1.2 → 0.1.4
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 +1 -1
- package/dist/cli/commands/compact.js +6 -0
- package/dist/cli/commands/init.js +184 -157
- package/dist/cli/commands/run.js +5 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/onboarding/InitTui.d.ts +52 -0
- package/dist/cli/onboarding/InitTui.js +153 -0
- package/dist/cli/onboarding/model.d.ts +39 -0
- package/dist/cli/onboarding/model.js +77 -0
- package/dist/cli/onboarding/should-use-tui.d.ts +25 -0
- package/dist/cli/onboarding/should-use-tui.js +35 -0
- package/dist/cli/onboarding/wordmark.d.ts +28 -0
- package/dist/cli/onboarding/wordmark.js +72 -0
- package/dist/core/aggregate-format.d.ts +3 -1
- package/dist/core/aggregate-format.js +18 -0
- package/dist/core/report-generator.d.ts +13 -0
- package/dist/core/report-generator.js +16 -0
- package/dist/core/run-aggregator.d.ts +19 -0
- package/dist/core/run-aggregator.js +68 -3
- package/dist/core/session-aggregate.d.ts +43 -0
- package/dist/core/session-aggregate.js +89 -7
- package/dist/core/types.d.ts +22 -0
- package/package.json +5 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout, render } from "ink";
|
|
4
|
+
import { VALUE_PROMISE, STATUS_PILLS, WORKFLOW_CARDS, FREE_FLOW, PRIMARY } from "./model.js";
|
|
5
|
+
import { renderWordmark, fillGradient } from "./wordmark.js";
|
|
6
|
+
// Restrained Compaction-blue palette only.
|
|
7
|
+
const BLUE = "#3231cd"; // brand accent / wordmark fill (bottom of gradient)
|
|
8
|
+
const OUTLINE = "#232185"; // one darker brand-blue tone for the framed-outline strokes
|
|
9
|
+
const DIM = "gray";
|
|
10
|
+
const FRAME_MAX = 96;
|
|
11
|
+
function Wordmark({ version, maxWidth }) {
|
|
12
|
+
const mark = useMemo(() => renderWordmark(maxWidth), [maxWidth]);
|
|
13
|
+
// Blue-only vertical gradient for the solid fill; the framed-outline strokes
|
|
14
|
+
// are one darker brand-blue tone (inline 3D depth, no contrasting colour).
|
|
15
|
+
const grad = useMemo(() => fillGradient(mark.height), [mark.height]);
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", children: [mark.rows.map((row, r) => (_jsx(Text, { children: row.map((seg, i) => {
|
|
17
|
+
if (seg.layer === "fill") {
|
|
18
|
+
return (_jsx(Text, { color: grad[r], bold: true, children: seg.text }, i));
|
|
19
|
+
}
|
|
20
|
+
if (seg.layer === "outline") {
|
|
21
|
+
return (_jsx(Text, { color: OUTLINE, children: seg.text }, i));
|
|
22
|
+
}
|
|
23
|
+
return _jsx(Text, { children: seg.text }, i);
|
|
24
|
+
}) }, r))), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: BLUE, bold: true, children: ["v", version] }), _jsx(Text, { color: DIM, children: " the Compaction CLI" })] })] }));
|
|
25
|
+
}
|
|
26
|
+
function StatusRow() {
|
|
27
|
+
return (_jsx(Box, { marginTop: 1, children: STATUS_PILLS.map((pill, i) => (_jsxs(Box, { children: [_jsx(Text, { color: BLUE, children: "\u25CF" }), _jsx(Text, { color: DIM, children: ` ${pill}` }), i < STATUS_PILLS.length - 1 ? _jsx(Text, { color: DIM, children: " " }) : null] }, pill))) }));
|
|
28
|
+
}
|
|
29
|
+
function badge(card, det, dim) {
|
|
30
|
+
if (card.key !== PRIMARY.key)
|
|
31
|
+
return null;
|
|
32
|
+
if (det.detected) {
|
|
33
|
+
return _jsx(Text, { color: dim ? DIM : BLUE, children: ` ✓ ${det.sessionCount} session(s) found` });
|
|
34
|
+
}
|
|
35
|
+
return _jsx(Text, { color: DIM, children: " · no sessions found yet" });
|
|
36
|
+
}
|
|
37
|
+
// The single highlighted card for the focused workflow. It hugs its content
|
|
38
|
+
// (alignSelf flex-start) so it reads as a card, not a full-width bar.
|
|
39
|
+
function ActiveCard({ card, det }) {
|
|
40
|
+
const recommended = card.key === PRIMARY.key;
|
|
41
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: BLUE, bold: true, children: recommended ? "▌ RECOMMENDED" : "▌ WORKFLOW" }), _jsxs(Box, { alignSelf: "flex-start", borderStyle: "round", borderColor: BLUE, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", bold: true, children: card.title }), recommended ? _jsx(Text, { color: BLUE, children: " ★" }) : null, badge(card, det, false)] }), _jsx(Text, { color: BLUE, children: card.command })] })] }));
|
|
42
|
+
}
|
|
43
|
+
// Quiet, compact, borderless row for an unfocused workflow.
|
|
44
|
+
function QuietRow({ card, det }) {
|
|
45
|
+
const recommended = card.key === PRIMARY.key;
|
|
46
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: DIM, children: " ○ " }), _jsx(Text, { color: DIM, children: card.title }), recommended ? _jsx(Text, { color: DIM, children: " ★ recommended" }) : null, badge(card, det, true)] }));
|
|
47
|
+
}
|
|
48
|
+
// Below this width the full chooser cannot lay out cleanly; show a compact
|
|
49
|
+
// recovery message instead. It re-renders into the full UI when widened again.
|
|
50
|
+
const NARROW_MIN = 44;
|
|
51
|
+
// Reactive terminal size: re-render the tree on `resize` (debounced) so the
|
|
52
|
+
// layout recomputes against the new width and Ink redraws cleanly. Reading the
|
|
53
|
+
// size into React state (rather than poking the terminal with escape codes) is
|
|
54
|
+
// what keeps the TUI visible across resizes. Guards columns/rows === 0/undefined.
|
|
55
|
+
function useTerminalSize() {
|
|
56
|
+
const { stdout } = useStdout();
|
|
57
|
+
const read = () => {
|
|
58
|
+
const c = stdout && typeof stdout.columns === "number" && stdout.columns > 0 ? stdout.columns : FRAME_MAX;
|
|
59
|
+
const r = stdout && typeof stdout.rows === "number" && stdout.rows > 0 ? stdout.rows : 24;
|
|
60
|
+
return { cols: c, rows: r };
|
|
61
|
+
};
|
|
62
|
+
const [size, setSize] = useState(read);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!stdout || typeof stdout.on !== "function")
|
|
65
|
+
return;
|
|
66
|
+
let t = null;
|
|
67
|
+
const onResize = () => {
|
|
68
|
+
if (t)
|
|
69
|
+
clearTimeout(t);
|
|
70
|
+
t = setTimeout(() => setSize(read()), 24); // debounce bursts of resize events
|
|
71
|
+
};
|
|
72
|
+
stdout.on("resize", onResize);
|
|
73
|
+
return () => {
|
|
74
|
+
if (t)
|
|
75
|
+
clearTimeout(t);
|
|
76
|
+
if (typeof stdout.off === "function")
|
|
77
|
+
stdout.off("resize", onResize);
|
|
78
|
+
};
|
|
79
|
+
// read() closes over `stdout`; re-subscribe only if the stream changes.
|
|
80
|
+
}, [stdout]);
|
|
81
|
+
return size;
|
|
82
|
+
}
|
|
83
|
+
export function App({ version, detection, onDone }) {
|
|
84
|
+
const [index, setIndex] = useState(0);
|
|
85
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
86
|
+
const [showCommands, setShowCommands] = useState(false);
|
|
87
|
+
const { exit } = useApp();
|
|
88
|
+
const { cols } = useTerminalSize();
|
|
89
|
+
const maxWidth = Math.min(cols, FRAME_MAX) - 2;
|
|
90
|
+
useInput((input, key) => {
|
|
91
|
+
if (key.upArrow || input === "k") {
|
|
92
|
+
setIndex((i) => (i - 1 + WORKFLOW_CARDS.length) % WORKFLOW_CARDS.length);
|
|
93
|
+
}
|
|
94
|
+
else if (key.downArrow || input === "j") {
|
|
95
|
+
setIndex((i) => (i + 1) % WORKFLOW_CARDS.length);
|
|
96
|
+
}
|
|
97
|
+
else if (key.return) {
|
|
98
|
+
onDone({ action: "select", card: WORKFLOW_CARDS[index] });
|
|
99
|
+
exit();
|
|
100
|
+
}
|
|
101
|
+
else if (input === "q" || key.escape) {
|
|
102
|
+
onDone({ action: "quit" });
|
|
103
|
+
exit();
|
|
104
|
+
}
|
|
105
|
+
else if (input === "?") {
|
|
106
|
+
setShowHelp((h) => !h);
|
|
107
|
+
}
|
|
108
|
+
else if (input === "/") {
|
|
109
|
+
setShowCommands((c) => !c);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// Too-narrow recovery: a clean compact message that re-expands into the full
|
|
113
|
+
// chooser when the terminal is widened again. q / Esc still quits (useInput
|
|
114
|
+
// above runs on every path).
|
|
115
|
+
if (cols < NARROW_MIN) {
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Text, { color: BLUE, bold: true, children: "COMPACTION" }), _jsx(Text, { color: DIM, children: "Terminal too narrow \u2014 widen the window to use the chooser," }), _jsx(Text, { color: DIM, children: "or run `compaction init --static`. (q / Esc to quit)" })] }));
|
|
117
|
+
}
|
|
118
|
+
const freeFlow = FREE_FLOW.map((f) => f.command.replace("compaction ", "")).join(" · ");
|
|
119
|
+
// Left-aligned, compact, no centering — the wordmark anchors the top-left.
|
|
120
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Wordmark, { version: version, maxWidth: maxWidth }), _jsx(StatusRow, {}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "white", children: VALUE_PROMISE }) }), _jsx(Box, { flexDirection: "column", children: WORKFLOW_CARDS.map((card, i) => i === index ? (_jsx(ActiveCard, { card: card, det: detection }, card.key)) : (_jsx(QuietRow, { card: card, det: detection }, card.key))) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: DIM, children: "Free local flow " }), _jsx(Text, { color: DIM, children: freeFlow })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: BLUE, bold: true, children: "Choose a workflow, press Enter" }), _jsx(Text, { color: DIM, children: " / commands ? help " }), _jsx(Text, { color: BLUE, children: "\u25CD local mode" })] }), _jsx(Text, { color: DIM, children: "↑/↓ · j/k move · Enter select · q / Esc quit" }), showCommands ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: DIM, children: "Free local flow, once you have a trace:" }), FREE_FLOW.map((f) => (_jsxs(Text, { children: [_jsx(Text, { color: BLUE, children: ` ${f.command}` }), _jsx(Text, { color: DIM, children: ` — ${f.desc}` })] }, f.command)))] })) : null, showHelp ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: DIM, children: `Enter prints the exact next command for ${WORKFLOW_CARDS[index].title} — copy & run` }), _jsx(Text, { color: DIM, children: "it. The CLI never runs it for you. Local-first: no network, no credentials, no upload." }), _jsx(Text, { color: DIM, children: "Nothing is called a saving until it is measured." })] })) : null] }));
|
|
121
|
+
}
|
|
122
|
+
const ALT_SCREEN_ENTER = "[?1049h[H[2J";
|
|
123
|
+
const ALT_SCREEN_LEAVE = "[?1049l";
|
|
124
|
+
/**
|
|
125
|
+
* Render the TUI and resolve with the user's choice. Resolves to `{action:"quit"}`
|
|
126
|
+
* if the user exits without selecting. The caller prints the chosen command to
|
|
127
|
+
* the scrollback after this resolves.
|
|
128
|
+
*
|
|
129
|
+
* Renders into the terminal's ALTERNATE SCREEN BUFFER (like vim/less/fzf): no
|
|
130
|
+
* scrollback, so a reflow can never push a stale frame up into history. Resize
|
|
131
|
+
* redraw is handled INSIDE the component (reactive terminal size → re-render),
|
|
132
|
+
* so Ink owns the clear/repaint and the screen stays visible across resizes. On
|
|
133
|
+
* exit the screen is restored and the selected next-command prints normally.
|
|
134
|
+
*/
|
|
135
|
+
export async function runInitTui(props) {
|
|
136
|
+
const out = process.stdout;
|
|
137
|
+
const useAlt = Boolean(out.isTTY);
|
|
138
|
+
if (useAlt)
|
|
139
|
+
out.write(ALT_SCREEN_ENTER);
|
|
140
|
+
let result = { action: "quit" };
|
|
141
|
+
try {
|
|
142
|
+
const app = render(_jsx(App, { version: props.version, detection: props.detection, onDone: (r) => {
|
|
143
|
+
result = r;
|
|
144
|
+
} }));
|
|
145
|
+
await app.waitUntilExit();
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (useAlt)
|
|
149
|
+
out.write(ALT_SCREEN_LEAVE);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=InitTui.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared onboarding model — the single source of truth for both the static
|
|
3
|
+
* `compaction init` fallback screen and the interactive Ink TUI.
|
|
4
|
+
*
|
|
5
|
+
* This module holds DATA ONLY (no chalk, no React) so it is trivially testable
|
|
6
|
+
* and importable from both the plain renderer and the .tsx component. The two
|
|
7
|
+
* surfaces must always feature the same workflows, the same free follow-ups,
|
|
8
|
+
* and the same honest claim labels — keeping them here guarantees that.
|
|
9
|
+
*
|
|
10
|
+
* Free-CLI scope: onboarding features ONLY the free, local-first commands
|
|
11
|
+
* (capture / import / analyze / spend / summary / feedback). The optimization
|
|
12
|
+
* & verification engine commands (recommend / compact / inspect / approve /
|
|
13
|
+
* apply-context) are the opt-in Compaction API — NOT in the free CLI — and so
|
|
14
|
+
* are deliberately ABSENT from this model on every surface.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BRAND_HEX = "#3231cd";
|
|
17
|
+
export declare const VALUE_PROMISE = "Find and reduce avoidable context spend in real agent workflows, locally.";
|
|
18
|
+
export declare const STATUS_PILLS: readonly ["local-first", "no telemetry", "no upload by default"];
|
|
19
|
+
export interface WorkflowCard {
|
|
20
|
+
key: string;
|
|
21
|
+
title: string;
|
|
22
|
+
/** The exact, copy-pasteable next command for this workflow. */
|
|
23
|
+
command: string;
|
|
24
|
+
/** One-line description of what running this workflow gets you. */
|
|
25
|
+
blurb: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const PRIMARY: WorkflowCard;
|
|
28
|
+
export declare const SECONDARY: WorkflowCard[];
|
|
29
|
+
/** All workflow cards, primary first — the order shown in the TUI chooser. */
|
|
30
|
+
export declare const WORKFLOW_CARDS: WorkflowCard[];
|
|
31
|
+
/** Lookup a card by key (used by `--path` focus and TUI selection). */
|
|
32
|
+
export declare function findWorkflow(key: string): WorkflowCard | undefined;
|
|
33
|
+
export declare const ALL_WORKFLOW_KEYS: string[];
|
|
34
|
+
export declare const FREE_FLOW: Array<{
|
|
35
|
+
command: string;
|
|
36
|
+
desc: string;
|
|
37
|
+
}>;
|
|
38
|
+
export declare const HONESTY_LINES: readonly ["Token counts are provider-reported when the run carries usage metadata,", "otherwise a local estimate (chars/4) — labeled as such. Cost is a", "price-table estimate; nothing is called a saving until it is measured.", "Optimization & verification are the opt-in Compaction API — not in the", "free CLI and not a hosted service yet. The free CLI measures and reports."];
|
|
39
|
+
export declare const FOOTER_LINES: readonly ["Local-first: no network, no credentials, no repo access for any of the above.", "A provider credential is only ever used if YOU explicitly run", "`compaction capture provider-usage` or enter operator-run records."];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared onboarding model — the single source of truth for both the static
|
|
3
|
+
* `compaction init` fallback screen and the interactive Ink TUI.
|
|
4
|
+
*
|
|
5
|
+
* This module holds DATA ONLY (no chalk, no React) so it is trivially testable
|
|
6
|
+
* and importable from both the plain renderer and the .tsx component. The two
|
|
7
|
+
* surfaces must always feature the same workflows, the same free follow-ups,
|
|
8
|
+
* and the same honest claim labels — keeping them here guarantees that.
|
|
9
|
+
*
|
|
10
|
+
* Free-CLI scope: onboarding features ONLY the free, local-first commands
|
|
11
|
+
* (capture / import / analyze / spend / summary / feedback). The optimization
|
|
12
|
+
* & verification engine commands (recommend / compact / inspect / approve /
|
|
13
|
+
* apply-context) are the opt-in Compaction API — NOT in the free CLI — and so
|
|
14
|
+
* are deliberately ABSENT from this model on every surface.
|
|
15
|
+
*/
|
|
16
|
+
// Brand accent. Used by both surfaces; chalk/ink down-sample or no-op it on
|
|
17
|
+
// terminals without truecolor, so the text is never lost.
|
|
18
|
+
export const BRAND_HEX = "#3231cd";
|
|
19
|
+
export const VALUE_PROMISE = "Find and reduce avoidable context spend in real agent workflows, locally.";
|
|
20
|
+
// The three short status pills shown under the wordmark on both surfaces.
|
|
21
|
+
export const STATUS_PILLS = ["local-first", "no telemetry", "no upload by default"];
|
|
22
|
+
// Real-workflow paths only. compaction operates on the user's OWN agent
|
|
23
|
+
// workflow; there is no demo / synthetic first-value path here.
|
|
24
|
+
export const PRIMARY = {
|
|
25
|
+
key: "claude-code",
|
|
26
|
+
title: "Claude Code",
|
|
27
|
+
command: "compaction capture claude-code --discover",
|
|
28
|
+
blurb: "Capture a local Claude Code session (recommended first run)."
|
|
29
|
+
};
|
|
30
|
+
export const SECONDARY = [
|
|
31
|
+
{
|
|
32
|
+
key: "openai-agents",
|
|
33
|
+
title: "OpenAI Agents SDK",
|
|
34
|
+
command: "compaction capture openai-agents --out ./my-trace -- <your-command>",
|
|
35
|
+
blurb: "Capture an OpenAI Agents SDK run."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "codex",
|
|
39
|
+
title: "Codex / local import",
|
|
40
|
+
command: "codex exec --json '<task>' > run.jsonl && compaction import run.jsonl --source codex-exec-jsonl --out ./my-trace",
|
|
41
|
+
blurb: "Import a Codex exec JSONL export."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "import",
|
|
45
|
+
title: "Local trace import",
|
|
46
|
+
command: "compaction import --list-sources # then: import <file> --source <source> --out ./my-trace",
|
|
47
|
+
blurb: "Import any local trace / JSONL you already have."
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
/** All workflow cards, primary first — the order shown in the TUI chooser. */
|
|
51
|
+
export const WORKFLOW_CARDS = [PRIMARY, ...SECONDARY];
|
|
52
|
+
/** Lookup a card by key (used by `--path` focus and TUI selection). */
|
|
53
|
+
export function findWorkflow(key) {
|
|
54
|
+
return WORKFLOW_CARDS.find((c) => c.key === key);
|
|
55
|
+
}
|
|
56
|
+
export const ALL_WORKFLOW_KEYS = WORKFLOW_CARDS.map((c) => c.key);
|
|
57
|
+
// The follow-up sequence once a trace exists — FREE commands only.
|
|
58
|
+
export const FREE_FLOW = [
|
|
59
|
+
{ command: "compaction analyze", desc: "input/output tokens + estimated spend, with honest labels" },
|
|
60
|
+
{ command: "compaction spend", desc: "where the context spend came from (attribution)" },
|
|
61
|
+
{ command: "compaction summary", desc: "roll up your local runs" },
|
|
62
|
+
{ command: "compaction feedback --redact", desc: "build a redacted, local-only share bundle (never uploaded)" }
|
|
63
|
+
];
|
|
64
|
+
// Honest claim labels, shown verbatim on both surfaces.
|
|
65
|
+
export const HONESTY_LINES = [
|
|
66
|
+
"Token counts are provider-reported when the run carries usage metadata,",
|
|
67
|
+
"otherwise a local estimate (chars/4) — labeled as such. Cost is a",
|
|
68
|
+
"price-table estimate; nothing is called a saving until it is measured.",
|
|
69
|
+
"Optimization & verification are the opt-in Compaction API — not in the",
|
|
70
|
+
"free CLI and not a hosted service yet. The free CLI measures and reports."
|
|
71
|
+
];
|
|
72
|
+
export const FOOTER_LINES = [
|
|
73
|
+
"Local-first: no network, no credentials, no repo access for any of the above.",
|
|
74
|
+
"A provider credential is only ever used if YOU explicitly run",
|
|
75
|
+
"`compaction capture provider-usage` or enter operator-run records."
|
|
76
|
+
];
|
|
77
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `compaction init` should launch the interactive Ink TUI or
|
|
3
|
+
* fall back to the plain static screen.
|
|
4
|
+
*
|
|
5
|
+
* The TUI is a progressive enhancement: it renders only in a real interactive
|
|
6
|
+
* terminal. Everywhere else — pipes, CI, NO_COLOR, dumb terminals, the test
|
|
7
|
+
* runner, or when the operator opts out — we serve the byte-stable static
|
|
8
|
+
* screen so scripts, snapshots, and smoke tests never see a control sequence.
|
|
9
|
+
*
|
|
10
|
+
* Pure function over an explicit environment snapshot so it is trivially unit
|
|
11
|
+
* testable without touching the real process.
|
|
12
|
+
*/
|
|
13
|
+
export interface TuiEnv {
|
|
14
|
+
stdoutIsTTY: boolean;
|
|
15
|
+
stdinIsTTY: boolean;
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
}
|
|
18
|
+
export interface TuiDecision {
|
|
19
|
+
useTui: boolean;
|
|
20
|
+
/** Machine-readable reason the TUI was declined (for tests / --help intuition). */
|
|
21
|
+
reason: "ok" | "not-a-tty" | "no-color" | "ci" | "dumb-term" | "opted-out" | "test-env";
|
|
22
|
+
}
|
|
23
|
+
export declare function decideInteractiveTui(e: TuiEnv): TuiDecision;
|
|
24
|
+
/** Convenience wrapper over the live process. */
|
|
25
|
+
export declare function decideInteractiveTuiFromProcess(): TuiDecision;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function decideInteractiveTui(e) {
|
|
2
|
+
// Raw-mode keyboard navigation requires BOTH a TTY stdin and stdout.
|
|
3
|
+
if (!e.stdoutIsTTY || !e.stdinIsTTY)
|
|
4
|
+
return { useTui: false, reason: "not-a-tty" };
|
|
5
|
+
// Honor NO_COLOR (no-color.org): present, regardless of value → plain output.
|
|
6
|
+
if (e.env.NO_COLOR != null)
|
|
7
|
+
return { useTui: false, reason: "no-color" };
|
|
8
|
+
// CI systems are non-interactive even when a pseudo-TTY is attached.
|
|
9
|
+
if (e.env.CI != null && e.env.CI !== "" && e.env.CI !== "false" && e.env.CI !== "0") {
|
|
10
|
+
return { useTui: false, reason: "ci" };
|
|
11
|
+
}
|
|
12
|
+
// Terminals that cannot render cursor movement / styling.
|
|
13
|
+
if (e.env.TERM === "dumb")
|
|
14
|
+
return { useTui: false, reason: "dumb-term" };
|
|
15
|
+
// Test runner — keep first-value output deterministic for snapshots.
|
|
16
|
+
if (e.env.VITEST != null || e.env.NODE_ENV === "test") {
|
|
17
|
+
return { useTui: false, reason: "test-env" };
|
|
18
|
+
}
|
|
19
|
+
// Explicit operator escape hatch. Accept any truthy value (1/true/yes/…),
|
|
20
|
+
// matching the lenient CI convention; "0"/"false"/"" do not opt out.
|
|
21
|
+
const optOut = e.env.COMPACTION_NO_TUI;
|
|
22
|
+
if (optOut != null && optOut !== "" && optOut !== "0" && optOut !== "false") {
|
|
23
|
+
return { useTui: false, reason: "opted-out" };
|
|
24
|
+
}
|
|
25
|
+
return { useTui: true, reason: "ok" };
|
|
26
|
+
}
|
|
27
|
+
/** Convenience wrapper over the live process. */
|
|
28
|
+
export function decideInteractiveTuiFromProcess() {
|
|
29
|
+
return decideInteractiveTui({
|
|
30
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
31
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
32
|
+
env: process.env
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=should-use-tui.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, dependency-free framed-outline block wordmark for the
|
|
3
|
+
* COMPACTION TUI hero.
|
|
4
|
+
*
|
|
5
|
+
* This restores the earlier "framed-outline / inline-outline" 3D treatment: the
|
|
6
|
+
* exact glyph art of figlet's "ANSI Shadow" font for the fixed word COMPACTION,
|
|
7
|
+
* BAKED IN as a constant (no figlet dependency — lean, deterministic, testable).
|
|
8
|
+
* Each glyph is a solid '█' fill framed by box-drawing outline strokes
|
|
9
|
+
* (╗ ╔ ═ ╝ ║ ╚) that give the inline 3D depth — NOT a separate offset shadow,
|
|
10
|
+
* NOT a dotted/ghost backdrop.
|
|
11
|
+
*
|
|
12
|
+
* The component paints the FILL with a restrained 2–3 shade Compaction-blue
|
|
13
|
+
* vertical gradient and the OUTLINE strokes with one darker brand-blue tone.
|
|
14
|
+
*/
|
|
15
|
+
export type Layer = "fill" | "outline" | "empty";
|
|
16
|
+
export interface Segment {
|
|
17
|
+
text: string;
|
|
18
|
+
layer: Layer;
|
|
19
|
+
}
|
|
20
|
+
export interface Wordmark {
|
|
21
|
+
rows: Segment[][];
|
|
22
|
+
/** "block" (framed-outline glyphs) or "text" (spaced fallback for narrow terminals). */
|
|
23
|
+
kind: "block" | "text";
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function renderWordmark(maxWidth: number): Wordmark;
|
|
28
|
+
export declare function fillGradient(rowCount: number): string[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, dependency-free framed-outline block wordmark for the
|
|
3
|
+
* COMPACTION TUI hero.
|
|
4
|
+
*
|
|
5
|
+
* This restores the earlier "framed-outline / inline-outline" 3D treatment: the
|
|
6
|
+
* exact glyph art of figlet's "ANSI Shadow" font for the fixed word COMPACTION,
|
|
7
|
+
* BAKED IN as a constant (no figlet dependency — lean, deterministic, testable).
|
|
8
|
+
* Each glyph is a solid '█' fill framed by box-drawing outline strokes
|
|
9
|
+
* (╗ ╔ ═ ╝ ║ ╚) that give the inline 3D depth — NOT a separate offset shadow,
|
|
10
|
+
* NOT a dotted/ghost backdrop.
|
|
11
|
+
*
|
|
12
|
+
* The component paints the FILL with a restrained 2–3 shade Compaction-blue
|
|
13
|
+
* vertical gradient and the OUTLINE strokes with one darker brand-blue tone.
|
|
14
|
+
*/
|
|
15
|
+
// Baked "ANSI Shadow" rendering of COMPACTION (figlet). Do not hand-edit the
|
|
16
|
+
// box-drawing characters; regenerate from figlet if the word ever changes.
|
|
17
|
+
const LINES = [
|
|
18
|
+
" ██████╗ ██████╗ ███╗ ███╗██████╗ █████╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗",
|
|
19
|
+
"██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║",
|
|
20
|
+
"██║ ██║ ██║██╔████╔██║██████╔╝███████║██║ ██║ ██║██║ ██║██╔██╗ ██║",
|
|
21
|
+
"██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██║ ██║ ██║██║ ██║██║╚██╗██║",
|
|
22
|
+
"╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║",
|
|
23
|
+
" ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝"
|
|
24
|
+
];
|
|
25
|
+
const FILL = "█";
|
|
26
|
+
const BLOCK_W = 83;
|
|
27
|
+
const SPACED = "C O M P A C T I O N";
|
|
28
|
+
// Classify each cell: solid fill, outline frame stroke, or empty.
|
|
29
|
+
function buildGrid() {
|
|
30
|
+
return LINES.map((line) => {
|
|
31
|
+
const padded = line.padEnd(BLOCK_W, " ");
|
|
32
|
+
const segs = [];
|
|
33
|
+
let cur = null;
|
|
34
|
+
for (const ch of padded) {
|
|
35
|
+
const layer = ch === " " ? "empty" : ch === FILL ? "fill" : "outline";
|
|
36
|
+
if (cur && cur.layer === layer)
|
|
37
|
+
cur.text += ch;
|
|
38
|
+
else {
|
|
39
|
+
cur = { layer, text: ch };
|
|
40
|
+
segs.push(cur);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return segs;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const BLOCK = buildGrid();
|
|
47
|
+
export function renderWordmark(maxWidth) {
|
|
48
|
+
if (maxWidth >= BLOCK_W) {
|
|
49
|
+
return { rows: BLOCK, kind: "block", width: BLOCK_W, height: BLOCK.length };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
rows: [[{ text: SPACED, layer: "fill" }]],
|
|
53
|
+
kind: "text",
|
|
54
|
+
width: SPACED.length,
|
|
55
|
+
height: 1
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Restrained blue-only vertical gradient for the FILL — at most three
|
|
59
|
+
// blue/indigo shades, no teal/cyan. One hex per row, sampled to the row count.
|
|
60
|
+
const BLUE_RAMP = ["#6d6bff", "#4f46e5", "#3231cd"];
|
|
61
|
+
export function fillGradient(rowCount) {
|
|
62
|
+
if (rowCount <= 1)
|
|
63
|
+
return [BLUE_RAMP[0]];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (let i = 0; i < rowCount; i++) {
|
|
66
|
+
const t = i / (rowCount - 1);
|
|
67
|
+
const idx = Math.min(BLUE_RAMP.length - 1, Math.round(t * (BLUE_RAMP.length - 1)));
|
|
68
|
+
out.push(BLUE_RAMP[idx]);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=wordmark.js.map
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* copies raw trace text, prompts, completions, tool output, file paths, run ids, or report
|
|
9
9
|
* paths into it. Like the feedback bundle, it is a LOCAL FILE ONLY: no network, no upload.
|
|
10
10
|
*/
|
|
11
|
-
import type { AggregateReport, AggregateTotals, VerificationCounts } from "./session-aggregate.js";
|
|
11
|
+
import type { AggregateReport, AggregateTotals, DistinctnessSummary, VerificationCounts } from "./session-aggregate.js";
|
|
12
12
|
import { AGGREGATE_EVIDENCE_LABELS } from "./session-aggregate.js";
|
|
13
13
|
import type { RunLabels } from "./run-labels.js";
|
|
14
14
|
export declare const SHARE_BUNDLE_VERSION = "1.0.0";
|
|
@@ -41,6 +41,8 @@ export interface AggregateShareBundle {
|
|
|
41
41
|
session_count: number;
|
|
42
42
|
totals: AggregateTotals;
|
|
43
43
|
verification: VerificationCounts;
|
|
44
|
+
/** Duplicate-safety summary (distinct N of M by content fingerprint) — counts only, no run ids. */
|
|
45
|
+
distinctness: DistinctnessSummary;
|
|
44
46
|
/** Provider/runtime breakdown — short label keys + numbers only (no run ids). */
|
|
45
47
|
by_provider: ShareBundleBreakdownEntry[];
|
|
46
48
|
by_project: ShareBundleBreakdownEntry[];
|
|
@@ -32,6 +32,18 @@ function totalsLines(totals) {
|
|
|
32
32
|
`estimated savings: ${usd(totals.total_estimated_savings_usd)} (estimated, summed over ${totals.compactions} recorded run(s); NOT billing-confirmed, NOT realized, NOT extrapolated)`
|
|
33
33
|
];
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* One honest distinctness line: "distinct runs N of M (K duplicates detected)" plus the
|
|
37
|
+
* fingerprint coverage. This is a count of distinct captured runs by content fingerprint — NOT a
|
|
38
|
+
* billing/provider/semantic claim. The headline savings are computed over the distinct set.
|
|
39
|
+
*/
|
|
40
|
+
function distinctnessLine(d) {
|
|
41
|
+
const noFp = d.runs_without_fingerprint > 0
|
|
42
|
+
? `; ${d.runs_without_fingerprint} run(s) without a fingerprint counted as distinct (cannot de-duplicate)`
|
|
43
|
+
: "";
|
|
44
|
+
return (`distinct runs ${d.distinct_runs} of ${d.total_runs} (${d.duplicate_runs} duplicate(s) detected by content fingerprint, ` +
|
|
45
|
+
`excluded from the headline so savings are not double-counted)${noFp}`);
|
|
46
|
+
}
|
|
35
47
|
function verificationLines(v) {
|
|
36
48
|
return [
|
|
37
49
|
`readiness: ready ${v.ready}, conditional ${v.conditional}, not_ready ${v.not_ready}, not_evaluated ${v.not_evaluated}`,
|
|
@@ -54,6 +66,7 @@ export function formatAggregateReport(report) {
|
|
|
54
66
|
lines.push(`Sessions: ${report.session_count}`);
|
|
55
67
|
lines.push("");
|
|
56
68
|
lines.push("Cross-session totals");
|
|
69
|
+
lines.push(`- ${distinctnessLine(report.distinctness)}`);
|
|
57
70
|
for (const line of totalsLines(report.totals))
|
|
58
71
|
lines.push(`- ${line}`);
|
|
59
72
|
lines.push("");
|
|
@@ -79,6 +92,7 @@ export function formatAggregateReport(report) {
|
|
|
79
92
|
labelBits.push(`labels: ${session.labels.user_labels.join("/")}`);
|
|
80
93
|
if (labelBits.length)
|
|
81
94
|
lines.push(` - ${labelBits.join("; ")}`);
|
|
95
|
+
lines.push(` - ${distinctnessLine(session.distinctness)}`);
|
|
82
96
|
for (const line of totalsLines(session.totals))
|
|
83
97
|
lines.push(` - ${line}`);
|
|
84
98
|
for (const line of verificationLines(session.verification))
|
|
@@ -141,6 +155,7 @@ export function formatAggregateMarkdown(report) {
|
|
|
141
155
|
lines.push("");
|
|
142
156
|
lines.push("## Cross-session totals");
|
|
143
157
|
lines.push("");
|
|
158
|
+
lines.push(`- Distinct runs: ${distinctnessLine(report.distinctness)}`);
|
|
144
159
|
lines.push(`- Runs (compactions): ${t.compactions}`);
|
|
145
160
|
lines.push(`- Token-evidence split: provider-reported ${t.runs_provider_reported}, local-estimate ${t.runs_local_estimate}, unknown ${t.runs_unknown_token_evidence}`);
|
|
146
161
|
lines.push(`- Total input tokens before: ${t.total_input_tokens_before}`);
|
|
@@ -219,6 +234,7 @@ export function formatAggregateMarkdown(report) {
|
|
|
219
234
|
}
|
|
220
235
|
const SHARE_INCLUDES = [
|
|
221
236
|
"aggregate token totals (input before/after/saved; output where recorded)",
|
|
237
|
+
"distinct-run count (distinct N of M by content fingerprint; duplicate-run count) — counts only",
|
|
222
238
|
"token-evidence split (provider-reported vs local-estimate run counts)",
|
|
223
239
|
"estimated cost + estimated savings totals (price-table estimate; NOT billing-confirmed)",
|
|
224
240
|
"compaction count + session count",
|
|
@@ -271,6 +287,7 @@ export function buildAggregateShareBundle(report, generatedAt = new Date().toISO
|
|
|
271
287
|
session_count: report.session_count,
|
|
272
288
|
totals: report.totals,
|
|
273
289
|
verification: report.verification,
|
|
290
|
+
distinctness: report.distinctness,
|
|
274
291
|
by_provider: breakdownEntries(report.by_provider),
|
|
275
292
|
by_project: breakdownEntries(report.by_project),
|
|
276
293
|
by_workflow: breakdownEntries(report.by_workflow),
|
|
@@ -299,6 +316,7 @@ export function formatShareBundleMarkdown(bundle) {
|
|
|
299
316
|
lines.push("");
|
|
300
317
|
lines.push("## Aggregate value (estimated, summed over recorded runs)");
|
|
301
318
|
lines.push("");
|
|
319
|
+
lines.push(`- Distinct runs: ${distinctnessLine(bundle.distinctness)}`);
|
|
302
320
|
lines.push(`- Compactions: ${t.compactions}`);
|
|
303
321
|
lines.push(`- Token-evidence split: provider-reported ${t.runs_provider_reported}, local-estimate ${t.runs_local_estimate}, unknown ${t.runs_unknown_token_evidence}`);
|
|
304
322
|
lines.push(`- Input tokens saved: ${t.total_input_tokens_saved}`);
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { SkillInjectionAdvisory } from "./skill-injection-detector.js";
|
|
2
2
|
import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, TokenEstimate, WasteFinding } from "./types.js";
|
|
3
3
|
import type { UsageMetadata } from "./usage-metadata.js";
|
|
4
|
+
/**
|
|
5
|
+
* Attach the content-addressed per-run trace fingerprint to a compaction report (PURE,
|
|
6
|
+
* additive, backward-compatible). This is the report-generation attachment point that
|
|
7
|
+
* threads trace identity into the savings-evidence pipeline so each per-run saving is
|
|
8
|
+
* attributable to a verifiable run identity and a re-captured run can be de-duplicated in
|
|
9
|
+
* the aggregate.
|
|
10
|
+
*
|
|
11
|
+
* It changes NO existing report field/label — it only sets the OPTIONAL `trace_fingerprint`
|
|
12
|
+
* from `computeTraceFingerprint(trace)` (a one-way SHA-256 digest + structural counts; no
|
|
13
|
+
* message content). Returns a new object; the input report is not mutated. This is an
|
|
14
|
+
* attribution/integrity signal, NOT a billing, provider, or semantic-preservation claim.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withTraceFingerprint(report: CompactionReport, trace: AgentTrace): CompactionReport;
|
|
4
17
|
export declare function formatAnalyzeReport(trace: AgentTrace, tokens: TokenEstimate, cost: CostEstimate, findings?: WasteFinding[], usage?: UsageMetadata, skillInjectionAdvisory?: SkillInjectionAdvisory): string;
|
|
5
18
|
export declare function formatCompactionReport(report: CompactionReport): string;
|
|
6
19
|
export declare function formatCompactionMarkdownReport(report: CompactionReport, policy: CompactionPolicy): string;
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { calculateCost } from "./cost-calculator.js";
|
|
2
2
|
import { isKnownModel } from "./pricing.js";
|
|
3
|
+
import { computeTraceFingerprint } from "./trace-fingerprint.js";
|
|
4
|
+
/**
|
|
5
|
+
* Attach the content-addressed per-run trace fingerprint to a compaction report (PURE,
|
|
6
|
+
* additive, backward-compatible). This is the report-generation attachment point that
|
|
7
|
+
* threads trace identity into the savings-evidence pipeline so each per-run saving is
|
|
8
|
+
* attributable to a verifiable run identity and a re-captured run can be de-duplicated in
|
|
9
|
+
* the aggregate.
|
|
10
|
+
*
|
|
11
|
+
* It changes NO existing report field/label — it only sets the OPTIONAL `trace_fingerprint`
|
|
12
|
+
* from `computeTraceFingerprint(trace)` (a one-way SHA-256 digest + structural counts; no
|
|
13
|
+
* message content). Returns a new object; the input report is not mutated. This is an
|
|
14
|
+
* attribution/integrity signal, NOT a billing, provider, or semantic-preservation claim.
|
|
15
|
+
*/
|
|
16
|
+
export function withTraceFingerprint(report, trace) {
|
|
17
|
+
return { ...report, trace_fingerprint: computeTraceFingerprint(trace) };
|
|
18
|
+
}
|
|
3
19
|
function formatTokenBreakdown(tokens) {
|
|
4
20
|
return `${tokens.totalTokens} total (${tokens.inputTokens} input, ${tokens.outputTokens} output)`;
|
|
5
21
|
}
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { CompactionReport, WasteFinding } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Duplicate-safety summary for the `summary` rollup, by content fingerprint. A count of DISTINCT
|
|
4
|
+
* captured runs — NOT a billing/provider/semantic claim. A run captured/aggregated twice with the
|
|
5
|
+
* SAME fingerprint is counted once in the headline so summed savings are never inflated; a run
|
|
6
|
+
* WITHOUT a fingerprint cannot be de-duplicated and is counted as distinct.
|
|
7
|
+
*/
|
|
8
|
+
export interface RunDistinctnessSummary {
|
|
9
|
+
total_runs: number;
|
|
10
|
+
distinct_runs: number;
|
|
11
|
+
duplicate_runs: number;
|
|
12
|
+
runs_with_fingerprint: number;
|
|
13
|
+
runs_without_fingerprint: number;
|
|
14
|
+
}
|
|
2
15
|
export interface RunSummaryBucket {
|
|
3
16
|
total_runs: number;
|
|
4
17
|
total_original_input_tokens: number;
|
|
@@ -28,6 +41,12 @@ export interface RunSummary extends RunSummaryBucket {
|
|
|
28
41
|
savings_by_waste_pattern: Record<string, RunSummaryBucket>;
|
|
29
42
|
top_savings_runs: TopSavingsRun[];
|
|
30
43
|
skipped_files: SkippedReportFile[];
|
|
44
|
+
/**
|
|
45
|
+
* Duplicate-safety summary: the headline totals above are summed over DISTINCT runs only (a
|
|
46
|
+
* run captured/aggregated twice with the same content fingerprint is counted once), so savings
|
|
47
|
+
* are never inflated. Runs without a fingerprint are each counted as distinct.
|
|
48
|
+
*/
|
|
49
|
+
distinctness: RunDistinctnessSummary;
|
|
31
50
|
generated_at: string;
|
|
32
51
|
source_glob: string;
|
|
33
52
|
}
|