@compaction/cli 0.1.2 → 0.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 +11 -1
- package/dist/cli/commands/compact.js +15 -0
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +130 -0
- package/dist/cli/commands/import.js +3 -1
- package/dist/cli/commands/init.js +184 -157
- package/dist/cli/commands/run.js +13 -1
- package/dist/cli/index.js +4 -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/context-store-eval-cases.d.ts +6 -0
- package/dist/core/context-store-eval-cases.js +331 -0
- package/dist/core/context-store-eval.d.ts +140 -0
- package/dist/core/context-store-eval.js +138 -0
- package/dist/core/context-store-fs.d.ts +73 -0
- package/dist/core/context-store-fs.js +157 -0
- package/dist/core/context-store-sufficiency.d.ts +98 -0
- package/dist/core/context-store-sufficiency.js +135 -0
- package/dist/core/context-store.d.ts +155 -0
- package/dist/core/context-store.js +228 -0
- package/dist/core/report-generator.d.ts +32 -1
- package/dist/core/report-generator.js +67 -0
- package/dist/core/run-aggregator.d.ts +64 -0
- package/dist/core/run-aggregator.js +132 -3
- package/dist/core/session-aggregate.d.ts +43 -0
- package/dist/core/session-aggregate.js +89 -7
- package/dist/core/trace-adapters.d.ts +8 -0
- package/dist/core/trace-adapters.js +4 -0
- package/dist/core/types.d.ts +55 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -24,6 +24,14 @@ Everything below runs on your machine and writes local files only:
|
|
|
24
24
|
billing-confirmed**.
|
|
25
25
|
- **Inspect spend and roll up runs.** `spend`, `summary`, and `aggregate` summarize where context
|
|
26
26
|
spend came from and roll up local runs — local files only, no hosted dashboard.
|
|
27
|
+
- **Build a local context store and retrieve from it.** `context add` accumulates source-pointed
|
|
28
|
+
items from a captured/imported trace into a local, size-capped store; `context get`
|
|
29
|
+
deterministically retrieves and assembles the active context for a query under a token budget,
|
|
30
|
+
printing it **with source pointers** plus local retrieval **diagnostics** (what was
|
|
31
|
+
considered / included / dropped, the recoverable fraction, latency). Local-only and
|
|
32
|
+
deterministic — **no model, no embeddings, no network**. It reports **diagnostics only** (no
|
|
33
|
+
quality or sufficiency verdict on your data) and makes **no savings claim** (see `spend` /
|
|
34
|
+
`summary` for cost).
|
|
27
35
|
- **Write local reports.** `analyze` writes local JSON + Markdown reports under an
|
|
28
36
|
operator-chosen output directory. Local files only.
|
|
29
37
|
- **Create redacted, content-free share bundles.** `feedback --redact` writes a redacted,
|
|
@@ -66,7 +74,7 @@ npx @compaction/cli --help # no install at all
|
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
The installed binary is `compaction`. After a global install, sanity-check with
|
|
69
|
-
`compaction --version` (→ `0.
|
|
77
|
+
`compaction --version` (→ `0.2.0`) and `compaction --help`.
|
|
70
78
|
|
|
71
79
|
### curl one-liner
|
|
72
80
|
|
|
@@ -133,6 +141,8 @@ compaction capture provider-usage --endpoint <url> [--credential-env-var <NAME>]
|
|
|
133
141
|
compaction import <trace-file> --source <source> --out <dir>
|
|
134
142
|
compaction import --list-sources
|
|
135
143
|
compaction analyze <trace-file>
|
|
144
|
+
compaction context add <trace-file> # build a local context store from a captured/imported trace
|
|
145
|
+
compaction context get "<query>" # retrieve + assemble the active context (with source pointers)
|
|
136
146
|
compaction spend <trace-or-optimization-path> # context-spend attribution
|
|
137
147
|
compaction summary # roll up local run reports
|
|
138
148
|
compaction aggregate # per-session / cross-session local rollups
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { Option } from "commander";
|
|
4
4
|
import { writeJsonArtifact, writeTextArtifact } from "../../core/artifact-writer.js";
|
|
5
|
+
import { formatCompactionMarkdownReport, withSavingsEvidence, withTraceFingerprint } from "../../core/report-generator.js";
|
|
5
6
|
import { describeTokenAccounting } from "../../core/token-accounting.js";
|
|
6
7
|
import { parseTraceFile } from "../../core/trace-parser.js";
|
|
7
8
|
import { buildRunLabelsFile, hasAnyLabel } from "../../core/run-labels.js";
|
|
@@ -123,6 +124,20 @@ export function registerCompactCommand(program) {
|
|
|
123
124
|
// hand-assembled bundle — the eval consumes the identical PolicyMiddlewareResult.
|
|
124
125
|
const policyResult = applyCompactionPolicy({ trace, compactSkillInjections: approveSkillInjectionPolicy });
|
|
125
126
|
const artifacts = await writeCompactionArtifacts(trace, options.out, runId, policyResult, undefined, { reviewerType, reviewSummaryPresent, approveSkillInjectionPolicy });
|
|
127
|
+
// Attribution/integrity + per-run evidence label (V0.2 savings-evidence): re-persist
|
|
128
|
+
// report.json with (1) the content-addressed trace fingerprint so `aggregate`/`summary` can
|
|
129
|
+
// attribute each saving to a verifiable run and de-duplicate a run rolled up twice, and
|
|
130
|
+
// (2) the composed `savings_evidence` record stating the per-run label explicitly. The
|
|
131
|
+
// compact path's figures are local estimates (chars/4) — `local_estimate` rung 1; never
|
|
132
|
+
// billing-confirmed, never semantic. Additive, backward-compatible; changes no existing field.
|
|
133
|
+
const evidenceReport = withSavingsEvidence(withTraceFingerprint(artifacts.report, trace), {
|
|
134
|
+
costSource: "local_estimate"
|
|
135
|
+
});
|
|
136
|
+
await writeJsonArtifact(options.out, "report.json", evidenceReport);
|
|
137
|
+
// Re-render report.md from the ENRICHED report so the per-run savings_evidence record
|
|
138
|
+
// appears in the human-readable artifact too (the engine rendered report.md from the base
|
|
139
|
+
// report before the CLI added the public-layer fingerprint/evidence enrichment).
|
|
140
|
+
await writeTextArtifact(options.out, "report.md", formatCompactionMarkdownReport(evidenceReport, artifacts.policy));
|
|
126
141
|
console.log(chalk.cyan("compaction compact"));
|
|
127
142
|
console.log(artifacts.consoleReport);
|
|
128
143
|
// Operator-visible only when the approval gesture was given; default output is unchanged.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { assembleContext, contextItemsFromTrace, retrieveContextItems } from "../../core/context-store.js";
|
|
5
|
+
import { appendContextItems, loadContextStore } from "../../core/context-store-fs.js";
|
|
6
|
+
/**
|
|
7
|
+
* V0.4 `compaction context` — a LOCAL memory + retrieval surface over the user's own captured
|
|
8
|
+
* traces (design: docs/design/v0.4-context-command.md, accepted 2026-06-24).
|
|
9
|
+
*
|
|
10
|
+
* HARD RAILS (load-bearing): local-only — no network, no model, no embeddings, no engine. It
|
|
11
|
+
* makes NO savings/cost claim (cost lives in `spend`/`summary`) and NO quality/recall/correctness
|
|
12
|
+
* verdict on user data (the four-axis sufficiency harness stays internal — there is no ground
|
|
13
|
+
* truth for an arbitrary user query). `get` prints local retrieval DIAGNOSTICS only.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_STORE_DIR = ".compaction/context-store";
|
|
16
|
+
function readTrace(path) {
|
|
17
|
+
if (!existsSync(path)) {
|
|
18
|
+
throw new Error(`File not found: ${path}`);
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error(`Not valid JSON: ${path}`);
|
|
26
|
+
}
|
|
27
|
+
const trace = parsed;
|
|
28
|
+
if (!trace || typeof trace.id !== "string" || !Array.isArray(trace.messages)) {
|
|
29
|
+
throw new Error("Not a normalized AgentTrace (expected { id, messages: [...] }). " +
|
|
30
|
+
"Produce one with `compaction capture` or `compaction import`.");
|
|
31
|
+
}
|
|
32
|
+
for (const message of trace.messages) {
|
|
33
|
+
const m = message;
|
|
34
|
+
if (!m || typeof m.id !== "string" || typeof m.content !== "string") {
|
|
35
|
+
throw new Error("Malformed trace: every message needs a string `id` and `content`. " +
|
|
36
|
+
"Produce a trace with `compaction capture` or `compaction import`.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
function collect(value, previous) {
|
|
42
|
+
return [...previous, value];
|
|
43
|
+
}
|
|
44
|
+
export function registerContextCommand(program) {
|
|
45
|
+
const context = program
|
|
46
|
+
.command("context")
|
|
47
|
+
.description("Local memory: build a durable context store from your own captured/imported traces and " +
|
|
48
|
+
"retrieve the active context for a next step (local-only; no model, no network, no savings claim).");
|
|
49
|
+
context
|
|
50
|
+
.command("add")
|
|
51
|
+
.argument("<artifact>", "Path to a local normalized AgentTrace JSON (from `compaction capture`/`import`)")
|
|
52
|
+
.option("--store-dir <dir>", "Context store directory (local, gitignored)", DEFAULT_STORE_DIR)
|
|
53
|
+
.description("Append context items derived from a local trace into the local context store (size-cap enforced).")
|
|
54
|
+
.action(async (artifact, options) => {
|
|
55
|
+
const storeDir = resolve(options.storeDir ?? DEFAULT_STORE_DIR);
|
|
56
|
+
const trace = readTrace(resolve(artifact));
|
|
57
|
+
const items = contextItemsFromTrace(trace, { created_at: new Date().toISOString() });
|
|
58
|
+
const result = await appendContextItems(storeDir, items);
|
|
59
|
+
console.log(chalk.cyan("compaction context add"));
|
|
60
|
+
console.log(`Source: ${artifact}`);
|
|
61
|
+
console.log(`Store: ${storeDir}`);
|
|
62
|
+
console.log(`Items added: ${result.appended.length} skipped (duplicate): ${result.skipped.length} ` +
|
|
63
|
+
`evicted (size cap): ${result.evicted.length}`);
|
|
64
|
+
console.log(`Store now holds: ${result.total} items.`);
|
|
65
|
+
console.log("Local-only: nothing was uploaded. This makes no savings claim (see `compaction spend`/`summary` for cost).");
|
|
66
|
+
console.log(chalk.bold('Next: compaction context get "<your query>"'));
|
|
67
|
+
});
|
|
68
|
+
context
|
|
69
|
+
.command("get")
|
|
70
|
+
.argument("<query>", "Free-text description of the current step")
|
|
71
|
+
.option("--store-dir <dir>", "Context store directory (local, gitignored)", DEFAULT_STORE_DIR)
|
|
72
|
+
.option("--budget <tokens>", "Token budget for the assembled context (local estimate)", "4000")
|
|
73
|
+
.option("--source <pointer>", "Required source pointer for coverage diagnostics (repeatable)", collect, [])
|
|
74
|
+
.option("--limit <n>", "Max candidate items to consider after ranking")
|
|
75
|
+
.description("Retrieve + assemble the active context for a query from the local store (deterministic, local). " +
|
|
76
|
+
"Prints the assembled context WITH source pointers + local retrieval diagnostics — not a quality score.")
|
|
77
|
+
.action(async (query, options) => {
|
|
78
|
+
const storeDir = resolve(options.storeDir ?? DEFAULT_STORE_DIR);
|
|
79
|
+
const budget = Number.parseInt(options.budget ?? "4000", 10);
|
|
80
|
+
if (Number.isNaN(budget) || budget < 0) {
|
|
81
|
+
console.error("--budget must be a non-negative integer number of tokens.");
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let limit;
|
|
86
|
+
if (options.limit !== undefined) {
|
|
87
|
+
limit = Number.parseInt(options.limit, 10);
|
|
88
|
+
if (Number.isNaN(limit) || limit < 0) {
|
|
89
|
+
console.error("--limit must be a non-negative integer.");
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const items = await loadContextStore(storeDir);
|
|
95
|
+
const retrievalOptions = limit !== undefined ? { limit } : {};
|
|
96
|
+
const start = performance.now();
|
|
97
|
+
const ranked = retrieveContextItems(items, { text: query, source_pointers: options.source && options.source.length > 0 ? options.source : undefined }, retrievalOptions);
|
|
98
|
+
const assembled = assembleContext(ranked, { tokenBudget: budget });
|
|
99
|
+
const latencyMs = performance.now() - start;
|
|
100
|
+
console.log(chalk.cyan("compaction context get"));
|
|
101
|
+
console.log(`Query: ${JSON.stringify(query)}`);
|
|
102
|
+
console.log(`Store: ${storeDir} (${items.length} items)`);
|
|
103
|
+
if (assembled.included.length === 0) {
|
|
104
|
+
console.log("Assembled context: (no matching context in the store for this query)");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(`Assembled context (~${assembled.estimated_tokens} tokens, local estimate; budget ${budget}):`);
|
|
108
|
+
for (const item of assembled.included) {
|
|
109
|
+
const pointer = item.source_pointer ?? "(no source pointer)";
|
|
110
|
+
const oneLine = item.content.replace(/\s+/g, " ");
|
|
111
|
+
console.log(` [${pointer}] ${oneLine}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const recoverable = assembled.included.filter((i) => i.recoverability === true && i.source_pointer).length;
|
|
115
|
+
console.log("Diagnostics (local retrieval, not a quality score):");
|
|
116
|
+
console.log(` considered: ${ranked.length} included: ${assembled.included.length} dropped: ${assembled.dropped.length}`);
|
|
117
|
+
console.log(` recoverable (has source pointer): ${recoverable}/${assembled.included.length}`);
|
|
118
|
+
if (options.source && options.source.length > 0) {
|
|
119
|
+
const includedPointers = new Set(assembled.included.map((i) => i.source_pointer).filter((p) => Boolean(p)));
|
|
120
|
+
const covered = options.source.filter((s) => includedPointers.has(s)).length;
|
|
121
|
+
console.log(` source coverage: ${covered}/${options.source.length}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(" source coverage: n/a (no --source given)");
|
|
125
|
+
}
|
|
126
|
+
console.log(` latency: ${latencyMs.toFixed(1)} ms`);
|
|
127
|
+
console.log("No model, no network. Local memory/retrieval view — it makes no savings claim and no quality/correctness claim.");
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -5,8 +5,10 @@ function printSupportedSources() {
|
|
|
5
5
|
console.log(chalk.cyan("compaction import sources"));
|
|
6
6
|
for (const adapter of listTraceAdapters()) {
|
|
7
7
|
console.log(`- ${adapter.id}: ${adapter.displayName}`);
|
|
8
|
+
console.log(` readiness: ${adapter.readiness}`);
|
|
8
9
|
}
|
|
9
|
-
console.log("
|
|
10
|
+
console.log("All sources are local file import (no live integration). Readiness above is an honest integration-readiness " +
|
|
11
|
+
"label (AGENTS.md Level 0–6) + validation status — not a stronger integration claim than the evidence supports.");
|
|
10
12
|
}
|
|
11
13
|
export function registerImportCommand(program) {
|
|
12
14
|
program
|
|
@@ -1,216 +1,243 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import chalk from "chalk";
|
|
3
6
|
import { defaultProjectsDir, discoverClaudeCodeSessions } from "../../core/adapters/claude-code-discovery.js";
|
|
7
|
+
import { BRAND_HEX, VALUE_PROMISE, STATUS_PILLS, PRIMARY, SECONDARY, FREE_FLOW, HONESTY_LINES, FOOTER_LINES, ALL_WORKFLOW_KEYS } from "../onboarding/model.js";
|
|
8
|
+
import { decideInteractiveTuiFromProcess } from "../onboarding/should-use-tui.js";
|
|
4
9
|
/**
|
|
5
10
|
* `compaction init` — first-run, value-led terminal onboarding.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
12
|
+
* In a real interactive terminal this launches a graphical Ink TUI (a workflow
|
|
13
|
+
* chooser). Everywhere else — pipes, CI, NO_COLOR, dumb terminals, the test
|
|
14
|
+
* runner, `--static`, or `--path <focus>` — it prints the byte-stable static
|
|
15
|
+
* screen. Both surfaces share ONE model (`../onboarding/model`) so they always
|
|
16
|
+
* feature the same workflows, the same free follow-ups, and the same honest
|
|
17
|
+
* claim labels.
|
|
10
18
|
*
|
|
11
|
-
* Invariants (non-negotiable, hold on
|
|
19
|
+
* Invariants (non-negotiable, hold on EVERY path — static and TUI):
|
|
12
20
|
* - writes nothing
|
|
13
21
|
* - makes no network call
|
|
14
22
|
* - reads no provider credential
|
|
15
23
|
* - needs no repo access
|
|
16
|
-
*
|
|
17
|
-
* `capture provider-usage` (or enters operator-run measurement records).
|
|
24
|
+
* The TUI is a chooser, not an agent: no text input, no model call, no upload.
|
|
18
25
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* Free-CLI scope: onboarding features ONLY the free, local-first commands
|
|
27
|
+
* (capture / import / analyze / spend / summary / feedback). The optimization &
|
|
28
|
+
* verification engine commands (recommend / compact / inspect / approve /
|
|
29
|
+
* apply-context) are the opt-in Compaction API — NOT in the free CLI — and so
|
|
30
|
+
* are deliberately NOT featured here.
|
|
24
31
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
32
|
+
* Honesty rails: token counts are provider-reported when usage metadata is
|
|
33
|
+
* present, otherwise a labeled local estimate (chars/4); cost is a price-table
|
|
34
|
+
* estimate; nothing is called a saving until it is measured. No billing-confirmed,
|
|
35
|
+
* no semantic-preservation, no hosted-API-live, no auto-apply, no demo-first.
|
|
28
36
|
*/
|
|
29
|
-
//
|
|
30
|
-
// terminal
|
|
31
|
-
const
|
|
37
|
+
// Brand accent. chalk.hex degrades gracefully: on a non-color / non-truecolor
|
|
38
|
+
// terminal chalk down-samples or no-ops, so the text is never lost.
|
|
39
|
+
const ACCENT = chalk.hex(BRAND_HEX);
|
|
40
|
+
// Spaced wordmark reads as a premium brand mark at any terminal width.
|
|
41
|
+
const WORDMARK = ACCENT.bold("C O M P A C T I O N");
|
|
32
42
|
const RULE = chalk.dim("─".repeat(58));
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
["compact", "the before/after token delta, and which family produced it"],
|
|
41
|
-
["inspect", "safety evidence — source pointers, hashes, recoverable originals"],
|
|
42
|
-
["approve", "emit a compacted context for your next call. No auto-apply."]
|
|
43
|
-
];
|
|
44
|
-
// Real-workflow paths only. compaction operates on the user's OWN agent
|
|
45
|
-
// workflow; there is no demo / synthetic first-value path here. Demo fixtures
|
|
46
|
-
// exist solely for tests, package smoke, and explicitly-labeled docs.
|
|
47
|
-
const PRIMARY = {
|
|
48
|
-
key: "claude-code",
|
|
49
|
-
title: "Claude Code",
|
|
50
|
-
command: "compaction capture claude-code --discover"
|
|
51
|
-
};
|
|
52
|
-
const SECONDARY = [
|
|
53
|
-
{
|
|
54
|
-
key: "codex",
|
|
55
|
-
title: "Codex",
|
|
56
|
-
command: "codex exec --json '<task>' > run.jsonl && compaction import run.jsonl --source codex-exec-jsonl --out ./my-trace"
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
key: "openai-agents",
|
|
60
|
-
title: "OpenAI Agents",
|
|
61
|
-
command: "compaction capture openai-agents --out ./my-trace -- <your-command>"
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
key: "import",
|
|
65
|
-
title: "Local trace",
|
|
66
|
-
command: "compaction import --list-sources # then: import <file> --source <source> --out ./my-trace"
|
|
43
|
+
async function readPackageVersion() {
|
|
44
|
+
try {
|
|
45
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
// dist/cli/commands -> dist -> package root
|
|
47
|
+
const pkgPath = path.resolve(here, "..", "..", "..", "package.json");
|
|
48
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
49
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
67
50
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
51
|
+
catch {
|
|
52
|
+
return "unknown";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function detectClaudeCode(projectsDir) {
|
|
56
|
+
let claudeDetected = false;
|
|
57
|
+
let sessionCount = 0;
|
|
58
|
+
if (existsSync(projectsDir)) {
|
|
59
|
+
try {
|
|
60
|
+
const discovery = await discoverClaudeCodeSessions({ projectsDir });
|
|
61
|
+
sessionCount = discovery.sessions.length;
|
|
62
|
+
claudeDetected = sessionCount > 0;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
claudeDetected = false; // detection is best-effort; onboarding never fails on it
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { claudeDetected, sessionCount, projectsDir };
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Static screen (the universal fallback). Output is intentionally byte-stable:
|
|
72
|
+
// scripts, snapshots, and smoke tests depend on it.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
76
74
|
function header(text) {
|
|
77
75
|
return chalk.bold(text);
|
|
78
76
|
}
|
|
79
|
-
function loopBlock() {
|
|
80
|
-
const width = Math.max(...LOOP.map(([verb]) => verb.length));
|
|
81
|
-
return LOOP.map(([verb, desc]) => ` ${chalk.cyan(verb.padEnd(width))} ${chalk.dim(desc)}`);
|
|
82
|
-
}
|
|
83
77
|
function primaryBlock(sessionCount) {
|
|
84
|
-
const tag = chalk.green(`
|
|
78
|
+
const tag = chalk.green(` ✓ ${sessionCount} session(s) found`);
|
|
85
79
|
return [
|
|
86
|
-
`${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended")}${tag}`,
|
|
87
|
-
` ${
|
|
80
|
+
`${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended first run")}${tag}`,
|
|
81
|
+
` ${ACCENT(PRIMARY.command)}`
|
|
88
82
|
];
|
|
89
83
|
}
|
|
90
|
-
function secondaryBlock() {
|
|
91
|
-
const lines = [chalk.dim("Other workflows")];
|
|
92
|
-
for (const p of SECONDARY) {
|
|
93
|
-
lines.push(` ${chalk.dim(p.title)}`);
|
|
94
|
-
lines.push(` ${chalk.cyan(p.command)}`);
|
|
95
|
-
}
|
|
96
|
-
return lines;
|
|
97
|
-
}
|
|
98
84
|
function emptyStateBlock(projectsDir) {
|
|
99
|
-
// Calm, premium no-sessions state: explain, show how to create one, and give
|
|
100
|
-
// the exact rerun command. The secondary real paths follow below.
|
|
101
85
|
return [
|
|
102
|
-
`${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended")}`,
|
|
86
|
+
`${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended first run")}`,
|
|
103
87
|
chalk.dim(` No sessions found under ${projectsDir}.`),
|
|
104
88
|
chalk.dim(" Start or run a Claude Code session in any project, then re-run:"),
|
|
105
|
-
` ${
|
|
89
|
+
` ${ACCENT("compaction capture claude-code --discover")}`
|
|
106
90
|
];
|
|
107
91
|
}
|
|
92
|
+
function supportedBlock() {
|
|
93
|
+
const summary = "Claude Code · OpenAI Agents SDK · Codex / local import · local trace import";
|
|
94
|
+
const lines = [header("Supported workflows"), ` ${chalk.dim(summary)}`, ""];
|
|
95
|
+
for (const p of SECONDARY) {
|
|
96
|
+
lines.push(` ${chalk.bold(p.title)}`);
|
|
97
|
+
lines.push(` ${ACCENT(p.command)}`);
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
108
101
|
function afterCaptureBlock() {
|
|
102
|
+
const width = Math.max(...FREE_FLOW.map((f) => f.command.length));
|
|
109
103
|
return [
|
|
110
104
|
header("Then, on the trace you captured"),
|
|
111
105
|
"",
|
|
112
|
-
...
|
|
106
|
+
...FREE_FLOW.map((f) => ` ${ACCENT(f.command.padEnd(width))} ${chalk.dim(f.desc)}`),
|
|
107
|
+
"",
|
|
108
|
+
chalk.dim(` ${HONESTY_LINES[0]}`),
|
|
109
|
+
chalk.dim(` ${HONESTY_LINES[1]}`),
|
|
110
|
+
chalk.dim(` ${HONESTY_LINES[2]}`),
|
|
113
111
|
"",
|
|
114
|
-
chalk.dim(
|
|
115
|
-
chalk.dim(
|
|
116
|
-
chalk.dim(" (provider-reported vs estimated — never an estimate as reported),"),
|
|
117
|
-
chalk.dim(" RISK LEVEL, safety + source_recoverability, the APPROVAL REQUIREMENT"),
|
|
118
|
-
chalk.dim(" (--approve-in-workflow-use; no auto-apply), and the next action.")
|
|
112
|
+
chalk.dim(` ${HONESTY_LINES[3]}`),
|
|
113
|
+
chalk.dim(` ${HONESTY_LINES[4]}`)
|
|
119
114
|
];
|
|
120
115
|
}
|
|
121
116
|
function footerBlock() {
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
return FOOTER_LINES.map((l) => chalk.dim(l));
|
|
118
|
+
}
|
|
119
|
+
function buildStaticScreen(version, focus, det) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
// 1. Wordmark + status + value promise.
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(` ${WORDMARK}`);
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push(` ${chalk.dim(`v${version} · ${STATUS_PILLS.join(" · ")}`)}`);
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push(` ${VALUE_PROMISE}`);
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push(RULE);
|
|
130
|
+
lines.push("");
|
|
131
|
+
// 2. Start here — primary path (Claude Code) or a focused secondary path.
|
|
132
|
+
if (!focus || focus === "claude-code") {
|
|
133
|
+
lines.push(header("Start here"));
|
|
134
|
+
lines.push("");
|
|
135
|
+
if (det.claudeDetected) {
|
|
136
|
+
lines.push(...primaryBlock(det.sessionCount));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
lines.push(...emptyStateBlock(det.projectsDir));
|
|
140
|
+
}
|
|
141
|
+
lines.push("");
|
|
142
|
+
if (!focus) {
|
|
143
|
+
lines.push(...supportedBlock());
|
|
144
|
+
lines.push("");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const p = SECONDARY.find((s) => s.key === focus);
|
|
149
|
+
lines.push(header("Start here"));
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push(` ${chalk.bold(p.title)}`);
|
|
152
|
+
lines.push(` ${ACCENT(p.command)}`);
|
|
153
|
+
lines.push("");
|
|
154
|
+
}
|
|
155
|
+
lines.push(RULE);
|
|
156
|
+
lines.push("");
|
|
157
|
+
// 3. The free follow-up sequence + honest labeling + opt-in-API positioning.
|
|
158
|
+
lines.push(...afterCaptureBlock());
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push(RULE);
|
|
161
|
+
lines.push("");
|
|
162
|
+
// 4. Local-first footer.
|
|
163
|
+
lines.push(...footerBlock());
|
|
164
|
+
lines.push("");
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
// After the TUI exits with a selection, print a clean, copy-ready next-command
|
|
168
|
+
// block to the scrollback so the user can paste and run it.
|
|
169
|
+
function printSelection(card) {
|
|
170
|
+
const out = [
|
|
171
|
+
"",
|
|
172
|
+
`${chalk.bold("▸ " + card.title)}`,
|
|
173
|
+
chalk.dim(` ${card.blurb}`),
|
|
174
|
+
"",
|
|
175
|
+
chalk.dim(" Run this next:"),
|
|
176
|
+
` ${ACCENT(card.command)}`,
|
|
177
|
+
"",
|
|
178
|
+
chalk.dim(` Then: ${FREE_FLOW.map((f) => f.command.replace("compaction ", "")).join(" · ")}`),
|
|
179
|
+
chalk.dim(" Local-first: no network, no credentials, no upload. Nothing is called a"),
|
|
180
|
+
chalk.dim(" saving until it is measured."),
|
|
181
|
+
""
|
|
126
182
|
];
|
|
183
|
+
console.log(out.join("\n"));
|
|
127
184
|
}
|
|
128
185
|
export function registerInitCommand(program) {
|
|
129
186
|
program
|
|
130
187
|
.command("init")
|
|
131
|
-
.description("First-run onboarding:
|
|
132
|
-
"
|
|
133
|
-
|
|
188
|
+
.description("First-run onboarding: an interactive workflow chooser in a real terminal (TUI), or the " +
|
|
189
|
+
"value promise + exact next commands as a static screen elsewhere. Guidance + one read-only " +
|
|
190
|
+
"local detection only — writes nothing, no network, no credentials.")
|
|
191
|
+
.option("--path <path>", "Focus one input path: claude-code | openai-agents | codex | import")
|
|
134
192
|
.option("--projects-dir <dir>", "Claude Code projects directory to detect against (default: the standard local location)")
|
|
193
|
+
.option("--interactive", "prefer the interactive TUI; if no TTY is available, show the static screen and why")
|
|
194
|
+
.option("--static", "always show the plain static screen (no TUI)")
|
|
135
195
|
.action(async (options) => {
|
|
136
196
|
// Validate --path early so the error stays a clean one-liner.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.error(`unknown --path '${options.path}'. Valid: ${allKeys.join(" | ")}`);
|
|
197
|
+
if (options.path && !ALL_WORKFLOW_KEYS.includes(options.path)) {
|
|
198
|
+
console.error(`unknown --path '${options.path}'. Valid: ${ALL_WORKFLOW_KEYS.join(" | ")}`);
|
|
140
199
|
process.exitCode = 1;
|
|
141
200
|
return;
|
|
142
201
|
}
|
|
202
|
+
// Version read at runtime so it always matches the installed package.
|
|
203
|
+
const version = await readPackageVersion();
|
|
143
204
|
// Read-only local detection (no writes, no network).
|
|
144
205
|
const projectsDir = options.projectsDir ?? defaultProjectsDir();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
claudeDetected = false; // detection is best-effort; onboarding never fails on it
|
|
155
|
-
}
|
|
156
|
-
}
|
|
206
|
+
const det = await detectClaudeCode(projectsDir);
|
|
207
|
+
// Decide static vs interactive. A focused path (--path) or --static always
|
|
208
|
+
// serves static; --interactive forces the TUI iff a TTY is present;
|
|
209
|
+
// otherwise auto-detect.
|
|
210
|
+
// Note: --interactive cannot conjure a TTY (raw-mode keyboard nav needs
|
|
211
|
+
// one), so it does not override the TTY check; it only documents intent.
|
|
212
|
+
// When asked for but unavailable, we fall back to static with a reason.
|
|
157
213
|
const focus = options.path;
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
lines.push("");
|
|
174
|
-
lines.push(RULE);
|
|
175
|
-
lines.push("");
|
|
176
|
-
// 3. Start here — primary path (Claude Code) or a focused secondary path.
|
|
177
|
-
if (!focus || focus === "claude-code") {
|
|
178
|
-
lines.push(header("Start here"));
|
|
179
|
-
lines.push("");
|
|
180
|
-
if (claudeDetected) {
|
|
181
|
-
lines.push(...primaryBlock(sessionCount));
|
|
214
|
+
const ttyDecision = decideInteractiveTuiFromProcess();
|
|
215
|
+
const wantTui = !focus && !options.static && ttyDecision.useTui;
|
|
216
|
+
if (wantTui) {
|
|
217
|
+
// Dynamic import keeps Ink/React off every non-TTY code path.
|
|
218
|
+
const { runInitTui } = await import("../onboarding/InitTui.js");
|
|
219
|
+
const result = await runInitTui({
|
|
220
|
+
version,
|
|
221
|
+
detection: {
|
|
222
|
+
detected: det.claudeDetected,
|
|
223
|
+
sessionCount: det.sessionCount,
|
|
224
|
+
projectsDir: det.projectsDir
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (result.action === "select") {
|
|
228
|
+
printSelection(result.card);
|
|
182
229
|
}
|
|
183
230
|
else {
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
lines.push("");
|
|
187
|
-
// 4. Secondary real-workflow paths (compact, secondary styling).
|
|
188
|
-
// Only on the default (unfocused) view.
|
|
189
|
-
if (!focus) {
|
|
190
|
-
lines.push(...secondaryBlock());
|
|
191
|
-
lines.push("");
|
|
231
|
+
console.log(chalk.dim("\nNo workflow selected. Run `compaction init` anytime.\n"));
|
|
192
232
|
}
|
|
233
|
+
return;
|
|
193
234
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
lines.push("");
|
|
199
|
-
lines.push(` ${chalk.bold(p.title)}`);
|
|
200
|
-
lines.push(` ${chalk.cyan(p.command)}`);
|
|
201
|
-
lines.push("");
|
|
235
|
+
// If the user explicitly asked for --interactive but we are not in a TTY,
|
|
236
|
+
// tell them why we fell back, then serve the static screen.
|
|
237
|
+
if (options.interactive && !ttyDecision.useTui) {
|
|
238
|
+
console.error(chalk.dim(`(interactive TUI unavailable here: ${ttyDecision.reason}; showing static screen)`));
|
|
202
239
|
}
|
|
203
|
-
|
|
204
|
-
lines.push("");
|
|
205
|
-
// 5. The follow-up sequence + what to expect.
|
|
206
|
-
lines.push(...afterCaptureBlock());
|
|
207
|
-
lines.push("");
|
|
208
|
-
lines.push(RULE);
|
|
209
|
-
lines.push("");
|
|
210
|
-
// 6. Local-first footer.
|
|
211
|
-
lines.push(...footerBlock());
|
|
212
|
-
lines.push("");
|
|
213
|
-
console.log(lines.join("\n"));
|
|
240
|
+
console.log(buildStaticScreen(version, focus, det));
|
|
214
241
|
});
|
|
215
242
|
}
|
|
216
243
|
//# sourceMappingURL=init.js.map
|