@hasna/terminal 2.2.0 → 2.3.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/dist/cli.js +29 -12
- package/package.json +1 -1
- package/src/ai.ts +50 -36
- package/src/cli.tsx +29 -12
- package/src/context-hints.ts +89 -0
- package/src/discover.ts +238 -0
- package/src/economy.ts +53 -0
- package/src/output-store.ts +65 -0
- package/src/providers/index.ts +4 -4
- package/src/sessions-db.ts +81 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
- package/temp/rtk/.claude/agents/debugger.md +519 -0
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
- package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
- package/temp/rtk/.claude/agents/technical-writer.md +355 -0
- package/temp/rtk/.claude/commands/diagnose.md +352 -0
- package/temp/rtk/.claude/commands/test-routing.md +362 -0
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
- package/temp/rtk/.claude/rules/cli-testing.md +526 -0
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
- package/temp/rtk/.claude/skills/performance.md +435 -0
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
- package/temp/rtk/.claude/skills/repo-recap.md +206 -0
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
- package/temp/rtk/.claude/skills/security-guardian.md +503 -0
- package/temp/rtk/.claude/skills/ship.md +404 -0
- package/temp/rtk/.github/workflows/benchmark.yml +34 -0
- package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
- package/temp/rtk/.github/workflows/release-please.yml +51 -0
- package/temp/rtk/.github/workflows/release.yml +343 -0
- package/temp/rtk/.github/workflows/security-check.yml +135 -0
- package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
- package/temp/rtk/.release-please-manifest.json +3 -0
- package/temp/rtk/ARCHITECTURE.md +1491 -0
- package/temp/rtk/CHANGELOG.md +640 -0
- package/temp/rtk/CLAUDE.md +605 -0
- package/temp/rtk/CONTRIBUTING.md +199 -0
- package/temp/rtk/Cargo.lock +1668 -0
- package/temp/rtk/Cargo.toml +64 -0
- package/temp/rtk/Formula/rtk.rb +43 -0
- package/temp/rtk/INSTALL.md +390 -0
- package/temp/rtk/LICENSE +21 -0
- package/temp/rtk/README.md +386 -0
- package/temp/rtk/README_es.md +159 -0
- package/temp/rtk/README_fr.md +197 -0
- package/temp/rtk/README_ja.md +159 -0
- package/temp/rtk/README_ko.md +159 -0
- package/temp/rtk/README_zh.md +167 -0
- package/temp/rtk/ROADMAP.md +15 -0
- package/temp/rtk/SECURITY.md +217 -0
- package/temp/rtk/TEST_EXEC_TIME.md +102 -0
- package/temp/rtk/build.rs +57 -0
- package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
- package/temp/rtk/docs/FEATURES.md +1410 -0
- package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
- package/temp/rtk/docs/filter-workflow.md +102 -0
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +583 -0
- package/temp/rtk/hooks/opencode-rtk.ts +39 -0
- package/temp/rtk/hooks/rtk-awareness.md +29 -0
- package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
- package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
- package/temp/rtk/install.sh +124 -0
- package/temp/rtk/release-please-config.json +10 -0
- package/temp/rtk/scripts/benchmark.sh +592 -0
- package/temp/rtk/scripts/check-installation.sh +162 -0
- package/temp/rtk/scripts/install-local.sh +37 -0
- package/temp/rtk/scripts/rtk-economics.sh +137 -0
- package/temp/rtk/scripts/test-all.sh +561 -0
- package/temp/rtk/scripts/test-aristote.sh +227 -0
- package/temp/rtk/scripts/test-tracking.sh +79 -0
- package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
- package/temp/rtk/scripts/validate-docs.sh +73 -0
- package/temp/rtk/src/aws_cmd.rs +880 -0
- package/temp/rtk/src/binlog.rs +1645 -0
- package/temp/rtk/src/cargo_cmd.rs +1727 -0
- package/temp/rtk/src/cc_economics.rs +1157 -0
- package/temp/rtk/src/ccusage.rs +340 -0
- package/temp/rtk/src/config.rs +187 -0
- package/temp/rtk/src/container.rs +855 -0
- package/temp/rtk/src/curl_cmd.rs +134 -0
- package/temp/rtk/src/deps.rs +268 -0
- package/temp/rtk/src/diff_cmd.rs +367 -0
- package/temp/rtk/src/discover/mod.rs +274 -0
- package/temp/rtk/src/discover/provider.rs +388 -0
- package/temp/rtk/src/discover/registry.rs +2022 -0
- package/temp/rtk/src/discover/report.rs +202 -0
- package/temp/rtk/src/discover/rules.rs +667 -0
- package/temp/rtk/src/display_helpers.rs +402 -0
- package/temp/rtk/src/dotnet_cmd.rs +1771 -0
- package/temp/rtk/src/dotnet_format_report.rs +133 -0
- package/temp/rtk/src/dotnet_trx.rs +593 -0
- package/temp/rtk/src/env_cmd.rs +204 -0
- package/temp/rtk/src/filter.rs +462 -0
- package/temp/rtk/src/filters/README.md +52 -0
- package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
- package/temp/rtk/src/filters/basedpyright.toml +47 -0
- package/temp/rtk/src/filters/biome.toml +45 -0
- package/temp/rtk/src/filters/brew-install.toml +37 -0
- package/temp/rtk/src/filters/composer-install.toml +40 -0
- package/temp/rtk/src/filters/df.toml +16 -0
- package/temp/rtk/src/filters/dotnet-build.toml +64 -0
- package/temp/rtk/src/filters/du.toml +16 -0
- package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
- package/temp/rtk/src/filters/gcc.toml +49 -0
- package/temp/rtk/src/filters/gcloud.toml +22 -0
- package/temp/rtk/src/filters/hadolint.toml +24 -0
- package/temp/rtk/src/filters/helm.toml +29 -0
- package/temp/rtk/src/filters/iptables.toml +27 -0
- package/temp/rtk/src/filters/jj.toml +28 -0
- package/temp/rtk/src/filters/jq.toml +24 -0
- package/temp/rtk/src/filters/make.toml +41 -0
- package/temp/rtk/src/filters/markdownlint.toml +24 -0
- package/temp/rtk/src/filters/mix-compile.toml +27 -0
- package/temp/rtk/src/filters/mix-format.toml +15 -0
- package/temp/rtk/src/filters/mvn-build.toml +44 -0
- package/temp/rtk/src/filters/oxlint.toml +43 -0
- package/temp/rtk/src/filters/ping.toml +63 -0
- package/temp/rtk/src/filters/pio-run.toml +40 -0
- package/temp/rtk/src/filters/poetry-install.toml +50 -0
- package/temp/rtk/src/filters/pre-commit.toml +35 -0
- package/temp/rtk/src/filters/ps.toml +16 -0
- package/temp/rtk/src/filters/quarto-render.toml +41 -0
- package/temp/rtk/src/filters/rsync.toml +48 -0
- package/temp/rtk/src/filters/shellcheck.toml +27 -0
- package/temp/rtk/src/filters/shopify-theme.toml +29 -0
- package/temp/rtk/src/filters/skopeo.toml +45 -0
- package/temp/rtk/src/filters/sops.toml +16 -0
- package/temp/rtk/src/filters/ssh.toml +44 -0
- package/temp/rtk/src/filters/stat.toml +34 -0
- package/temp/rtk/src/filters/swift-build.toml +41 -0
- package/temp/rtk/src/filters/systemctl-status.toml +33 -0
- package/temp/rtk/src/filters/terraform-plan.toml +35 -0
- package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
- package/temp/rtk/src/filters/tofu-init.toml +38 -0
- package/temp/rtk/src/filters/tofu-plan.toml +35 -0
- package/temp/rtk/src/filters/tofu-validate.toml +17 -0
- package/temp/rtk/src/filters/trunk-build.toml +39 -0
- package/temp/rtk/src/filters/ty.toml +50 -0
- package/temp/rtk/src/filters/uv-sync.toml +37 -0
- package/temp/rtk/src/filters/xcodebuild.toml +99 -0
- package/temp/rtk/src/filters/yamllint.toml +25 -0
- package/temp/rtk/src/find_cmd.rs +598 -0
- package/temp/rtk/src/format_cmd.rs +386 -0
- package/temp/rtk/src/gain.rs +723 -0
- package/temp/rtk/src/gh_cmd.rs +1651 -0
- package/temp/rtk/src/git.rs +2012 -0
- package/temp/rtk/src/go_cmd.rs +592 -0
- package/temp/rtk/src/golangci_cmd.rs +254 -0
- package/temp/rtk/src/grep_cmd.rs +288 -0
- package/temp/rtk/src/gt_cmd.rs +810 -0
- package/temp/rtk/src/hook_audit_cmd.rs +283 -0
- package/temp/rtk/src/hook_check.rs +171 -0
- package/temp/rtk/src/init.rs +1859 -0
- package/temp/rtk/src/integrity.rs +537 -0
- package/temp/rtk/src/json_cmd.rs +231 -0
- package/temp/rtk/src/learn/detector.rs +628 -0
- package/temp/rtk/src/learn/mod.rs +119 -0
- package/temp/rtk/src/learn/report.rs +184 -0
- package/temp/rtk/src/lint_cmd.rs +694 -0
- package/temp/rtk/src/local_llm.rs +316 -0
- package/temp/rtk/src/log_cmd.rs +248 -0
- package/temp/rtk/src/ls.rs +324 -0
- package/temp/rtk/src/main.rs +2482 -0
- package/temp/rtk/src/mypy_cmd.rs +389 -0
- package/temp/rtk/src/next_cmd.rs +241 -0
- package/temp/rtk/src/npm_cmd.rs +236 -0
- package/temp/rtk/src/parser/README.md +267 -0
- package/temp/rtk/src/parser/error.rs +46 -0
- package/temp/rtk/src/parser/formatter.rs +336 -0
- package/temp/rtk/src/parser/mod.rs +311 -0
- package/temp/rtk/src/parser/types.rs +119 -0
- package/temp/rtk/src/pip_cmd.rs +302 -0
- package/temp/rtk/src/playwright_cmd.rs +479 -0
- package/temp/rtk/src/pnpm_cmd.rs +573 -0
- package/temp/rtk/src/prettier_cmd.rs +221 -0
- package/temp/rtk/src/prisma_cmd.rs +482 -0
- package/temp/rtk/src/psql_cmd.rs +382 -0
- package/temp/rtk/src/pytest_cmd.rs +384 -0
- package/temp/rtk/src/read.rs +217 -0
- package/temp/rtk/src/rewrite_cmd.rs +50 -0
- package/temp/rtk/src/ruff_cmd.rs +402 -0
- package/temp/rtk/src/runner.rs +271 -0
- package/temp/rtk/src/summary.rs +297 -0
- package/temp/rtk/src/tee.rs +405 -0
- package/temp/rtk/src/telemetry.rs +248 -0
- package/temp/rtk/src/toml_filter.rs +1655 -0
- package/temp/rtk/src/tracking.rs +1416 -0
- package/temp/rtk/src/tree.rs +209 -0
- package/temp/rtk/src/tsc_cmd.rs +259 -0
- package/temp/rtk/src/utils.rs +432 -0
- package/temp/rtk/src/verify_cmd.rs +47 -0
- package/temp/rtk/src/vitest_cmd.rs +385 -0
- package/temp/rtk/src/wc_cmd.rs +401 -0
- package/temp/rtk/src/wget_cmd.rs +260 -0
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ SUBCOMMANDS:
|
|
|
28
28
|
collection create|list Recipe collections
|
|
29
29
|
mcp serve Start MCP server for AI agents
|
|
30
30
|
mcp install --claude|--codex Install MCP server
|
|
31
|
+
discover [--days=N] [--json] Scan Claude sessions, show token savings potential
|
|
31
32
|
snapshot Terminal state as JSON
|
|
32
33
|
--help Show this help
|
|
33
34
|
--version Show version
|
|
@@ -253,17 +254,8 @@ else if (args[0] === "collection") {
|
|
|
253
254
|
}
|
|
254
255
|
// ── Stats command ────────────────────────────────────────────────────────────
|
|
255
256
|
else if (args[0] === "stats") {
|
|
256
|
-
const {
|
|
257
|
-
|
|
258
|
-
console.log("Token Economy:");
|
|
259
|
-
console.log(` Total saved: ${formatTokens(s.totalTokensSaved)}`);
|
|
260
|
-
console.log(` Total used: ${formatTokens(s.totalTokensUsed)}`);
|
|
261
|
-
console.log(` By feature:`);
|
|
262
|
-
console.log(` Structured: ${formatTokens(s.savingsByFeature.structured)}`);
|
|
263
|
-
console.log(` Compressed: ${formatTokens(s.savingsByFeature.compressed)}`);
|
|
264
|
-
console.log(` Diff cache: ${formatTokens(s.savingsByFeature.diff)}`);
|
|
265
|
-
console.log(` NL cache: ${formatTokens(s.savingsByFeature.cache)}`);
|
|
266
|
-
console.log(` Search: ${formatTokens(s.savingsByFeature.search)}`);
|
|
257
|
+
const { formatEconomicsSummary } = await import("./economy.js");
|
|
258
|
+
console.log(formatEconomicsSummary());
|
|
267
259
|
}
|
|
268
260
|
// ── Sessions command ─────────────────────────────────────────────────────────
|
|
269
261
|
else if (args[0] === "sessions") {
|
|
@@ -407,6 +399,19 @@ else if (args[0] === "explain" && args[1]) {
|
|
|
407
399
|
const explanation = await explainCommand(command);
|
|
408
400
|
console.log(explanation);
|
|
409
401
|
}
|
|
402
|
+
// ── Discover command ─────────────────────────────────────────────────────────
|
|
403
|
+
else if (args[0] === "discover") {
|
|
404
|
+
const { discover, formatDiscoverReport } = await import("./discover.js");
|
|
405
|
+
const days = parseInt(args.find(a => a.startsWith("--days="))?.split("=")[1] ?? "30");
|
|
406
|
+
const json = args.includes("--json");
|
|
407
|
+
const report = discover({ maxAgeDays: days });
|
|
408
|
+
if (json) {
|
|
409
|
+
console.log(JSON.stringify(report, null, 2));
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
console.log(formatDiscoverReport(report));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
410
415
|
// ── Snapshot command ─────────────────────────────────────────────────────────
|
|
411
416
|
else if (args[0] === "snapshot") {
|
|
412
417
|
const { captureSnapshot } = await import("./snapshots.js");
|
|
@@ -430,6 +435,7 @@ else if (args.length > 0) {
|
|
|
430
435
|
const { processOutput, shouldProcess } = await import("./output-processor.js");
|
|
431
436
|
const { rewriteCommand } = await import("./command-rewriter.js");
|
|
432
437
|
const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
|
|
438
|
+
const { saveOutput, formatOutputHint } = await import("./output-store.js");
|
|
433
439
|
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
434
440
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
435
441
|
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
@@ -437,6 +443,7 @@ else if (args.length > 0) {
|
|
|
437
443
|
const { loadConfig } = await import("./history.js");
|
|
438
444
|
const { loadContext, saveContext, formatContext } = await import("./session-context.js");
|
|
439
445
|
const { getLearned, recordMapping } = await import("./usage-cache.js");
|
|
446
|
+
const { recordCorrection, findSimilarCorrections, recordOutput } = await import("./sessions-db.js");
|
|
440
447
|
const config = loadConfig();
|
|
441
448
|
const perms = config.permissions;
|
|
442
449
|
const sessionCtx = formatContext();
|
|
@@ -580,7 +587,15 @@ else if (args.length > 0) {
|
|
|
580
587
|
if (processed.aiProcessed) {
|
|
581
588
|
if (processed.tokensSaved > 0)
|
|
582
589
|
recordSaving("compressed", processed.tokensSaved);
|
|
583
|
-
|
|
590
|
+
// Save full output for lazy recovery — agents can read the file
|
|
591
|
+
if (processed.tokensSaved > 50) {
|
|
592
|
+
const outputPath = saveOutput(actualCmd, clean);
|
|
593
|
+
console.log(processed.summary);
|
|
594
|
+
console.log(formatOutputHint(outputPath));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
console.log(processed.summary);
|
|
598
|
+
}
|
|
584
599
|
if (processed.tokensSaved > 10)
|
|
585
600
|
console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
|
|
586
601
|
process.exit(0);
|
|
@@ -637,6 +652,8 @@ else if (args.length > 0) {
|
|
|
637
652
|
const retryResult = execSync(retryCmd + " #(retry)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
638
653
|
const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
|
|
639
654
|
if (retryClean.length > 5) {
|
|
655
|
+
// Record the correction so we learn from it
|
|
656
|
+
recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
|
|
640
657
|
const processed = await processOutput(retryCmd, retryClean, prompt);
|
|
641
658
|
console.log(processed.aiProcessed ? processed.summary : retryClean);
|
|
642
659
|
process.exit(0);
|
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -6,51 +6,48 @@ import { join } from "path";
|
|
|
6
6
|
import { discoverProjectHints, discoverSafetyHints, formatHints } from "./context-hints.js";
|
|
7
7
|
|
|
8
8
|
// ── model routing ─────────────────────────────────────────────────────────────
|
|
9
|
-
//
|
|
9
|
+
// Config-driven model selection. Defaults per provider, user can override in ~/.terminal/config.json
|
|
10
10
|
|
|
11
11
|
const COMPLEX_SIGNALS = [
|
|
12
12
|
/\b(undo|revert|rollback|previous|last)\b/i,
|
|
13
13
|
/\b(all files?|recursively|bulk|batch)\b/i,
|
|
14
14
|
/\b(pipeline|chain|then|and then|after)\b/i,
|
|
15
15
|
/\b(if|when|unless|only if)\b/i,
|
|
16
|
-
/\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i,
|
|
17
|
-
/\b(inside|within|under)\b/i,
|
|
18
|
-
/[|&;]{2}/,
|
|
16
|
+
/\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i,
|
|
17
|
+
/\b(inside|within|under)\b/i,
|
|
18
|
+
/[|&;]{2}/,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/** Default models per provider — user can override in ~/.terminal/config.json under "models" */
|
|
22
|
+
const MODEL_DEFAULTS: Record<string, { fast: string; smart: string }> = {
|
|
23
|
+
cerebras: { fast: "qwen-3-235b-a22b-instruct-2507", smart: "qwen-3-235b-a22b-instruct-2507" },
|
|
24
|
+
groq: { fast: "openai/gpt-oss-120b", smart: "moonshotai/kimi-k2-instruct" },
|
|
25
|
+
xai: { fast: "grok-code-fast-1", smart: "grok-4-fast-non-reasoning" },
|
|
26
|
+
anthropic: { fast: "claude-haiku-4-5-20251001", smart: "claude-sonnet-4-6" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Load user model overrides from ~/.terminal/config.json */
|
|
30
|
+
function loadModelOverrides(): Record<string, { fast?: string; smart?: string }> {
|
|
31
|
+
try {
|
|
32
|
+
const configPath = join(process.env.HOME ?? "~", ".terminal", "config.json");
|
|
33
|
+
if (existsSync(configPath)) {
|
|
34
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
35
|
+
return config.models ?? {};
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Model routing per provider — config-driven with defaults */
|
|
22
42
|
function pickModel(nl: string): { fast: string; smart: string; pick: "fast" | "smart" } {
|
|
23
43
|
const isComplex = COMPLEX_SIGNALS.some((r) => r.test(nl)) || nl.split(" ").length > 10;
|
|
24
44
|
const provider = getProvider();
|
|
45
|
+
const defaults = MODEL_DEFAULTS[provider.name] ?? MODEL_DEFAULTS.cerebras;
|
|
46
|
+
const overrides = loadModelOverrides()[provider.name] ?? {};
|
|
25
47
|
|
|
26
|
-
if (provider.name === "anthropic") {
|
|
27
|
-
return {
|
|
28
|
-
fast: "claude-haiku-4-5-20251001",
|
|
29
|
-
smart: "claude-sonnet-4-6",
|
|
30
|
-
pick: isComplex ? "smart" : "fast",
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (provider.name === "groq") {
|
|
35
|
-
return {
|
|
36
|
-
fast: "openai/gpt-oss-120b",
|
|
37
|
-
smart: "moonshotai/kimi-k2-instruct",
|
|
38
|
-
pick: isComplex ? "smart" : "fast",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (provider.name === "xai") {
|
|
43
|
-
return {
|
|
44
|
-
fast: "grok-code-fast-1",
|
|
45
|
-
smart: "grok-4-fast-non-reasoning",
|
|
46
|
-
pick: isComplex ? "smart" : "fast",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Cerebras — qwen for everything (llama3.1-8b too unreliable)
|
|
51
48
|
return {
|
|
52
|
-
fast:
|
|
53
|
-
smart:
|
|
49
|
+
fast: overrides.fast ?? defaults.fast,
|
|
50
|
+
smart: overrides.smart ?? defaults.smart,
|
|
54
51
|
pick: isComplex ? "smart" : "fast",
|
|
55
52
|
};
|
|
56
53
|
}
|
|
@@ -124,6 +121,23 @@ export interface SessionEntry {
|
|
|
124
121
|
error?: boolean;
|
|
125
122
|
}
|
|
126
123
|
|
|
124
|
+
// ── correction memory ───────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/** Load past corrections relevant to a prompt — injected as negative examples */
|
|
127
|
+
function loadCorrectionHints(prompt: string): string {
|
|
128
|
+
try {
|
|
129
|
+
// Dynamic import to avoid circular deps
|
|
130
|
+
const { findSimilarCorrections } = require("./sessions-db.js");
|
|
131
|
+
const corrections = findSimilarCorrections(prompt, 3);
|
|
132
|
+
if (corrections.length === 0) return "";
|
|
133
|
+
|
|
134
|
+
const lines = corrections.map((c: any) =>
|
|
135
|
+
`AVOID: "${c.failed_command}" (failed: ${c.error_type}). USE: "${c.corrected_command}" instead.`
|
|
136
|
+
);
|
|
137
|
+
return `\n\nLEARNED CORRECTIONS (from past failures):\n${lines.join("\n")}`;
|
|
138
|
+
} catch { return ""; }
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
// ── project context (powered by context-hints) ──────────────────────────────
|
|
128
142
|
|
|
129
143
|
function detectProjectContext(): string {
|
|
@@ -133,7 +147,7 @@ function detectProjectContext(): string {
|
|
|
133
147
|
|
|
134
148
|
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
135
149
|
|
|
136
|
-
function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[]): string {
|
|
150
|
+
function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[], currentPrompt?: string): string {
|
|
137
151
|
const restrictions: string[] = [];
|
|
138
152
|
if (!perms.destructive)
|
|
139
153
|
restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
|
|
@@ -233,7 +247,7 @@ EXISTENCE CHECKS: If the prompt starts with "is there", "does this have", "do we
|
|
|
233
247
|
|
|
234
248
|
MONOREPO: If the project context says "MONOREPO", search packages/ or apps/ NOT src/. Use: grep -rn "pattern" packages/ --include="*.ts". For specific packages, use packages/PKGNAME/src/.
|
|
235
249
|
cwd: ${process.cwd()}
|
|
236
|
-
shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}`;
|
|
250
|
+
shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}`;
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
// ── streaming translate ───────────────────────────────────────────────────────
|
|
@@ -253,7 +267,7 @@ export async function translateToCommand(
|
|
|
253
267
|
const provider = getProvider();
|
|
254
268
|
const routing = pickModel(nl);
|
|
255
269
|
const model = routing.pick === "smart" ? routing.smart : routing.fast;
|
|
256
|
-
const system = buildSystemPrompt(perms, sessionEntries);
|
|
270
|
+
const system = buildSystemPrompt(perms, sessionEntries, nl);
|
|
257
271
|
|
|
258
272
|
let text: string;
|
|
259
273
|
|
|
@@ -332,7 +346,7 @@ export async function fixCommand(
|
|
|
332
346
|
{
|
|
333
347
|
model: routing.smart, // always use smart model for fixes
|
|
334
348
|
maxTokens: 256,
|
|
335
|
-
system: buildSystemPrompt(perms, sessionEntries),
|
|
349
|
+
system: buildSystemPrompt(perms, sessionEntries, originalNl),
|
|
336
350
|
}
|
|
337
351
|
);
|
|
338
352
|
if (text.startsWith("BLOCKED:")) throw new Error(text);
|
package/src/cli.tsx
CHANGED
|
@@ -31,6 +31,7 @@ SUBCOMMANDS:
|
|
|
31
31
|
collection create|list Recipe collections
|
|
32
32
|
mcp serve Start MCP server for AI agents
|
|
33
33
|
mcp install --claude|--codex Install MCP server
|
|
34
|
+
discover [--days=N] [--json] Scan Claude sessions, show token savings potential
|
|
34
35
|
snapshot Terminal state as JSON
|
|
35
36
|
--help Show this help
|
|
36
37
|
--version Show version
|
|
@@ -243,17 +244,8 @@ else if (args[0] === "collection") {
|
|
|
243
244
|
// ── Stats command ────────────────────────────────────────────────────────────
|
|
244
245
|
|
|
245
246
|
else if (args[0] === "stats") {
|
|
246
|
-
const {
|
|
247
|
-
|
|
248
|
-
console.log("Token Economy:");
|
|
249
|
-
console.log(` Total saved: ${formatTokens(s.totalTokensSaved)}`);
|
|
250
|
-
console.log(` Total used: ${formatTokens(s.totalTokensUsed)}`);
|
|
251
|
-
console.log(` By feature:`);
|
|
252
|
-
console.log(` Structured: ${formatTokens(s.savingsByFeature.structured)}`);
|
|
253
|
-
console.log(` Compressed: ${formatTokens(s.savingsByFeature.compressed)}`);
|
|
254
|
-
console.log(` Diff cache: ${formatTokens(s.savingsByFeature.diff)}`);
|
|
255
|
-
console.log(` NL cache: ${formatTokens(s.savingsByFeature.cache)}`);
|
|
256
|
-
console.log(` Search: ${formatTokens(s.savingsByFeature.search)}`);
|
|
247
|
+
const { formatEconomicsSummary } = await import("./economy.js");
|
|
248
|
+
console.log(formatEconomicsSummary());
|
|
257
249
|
}
|
|
258
250
|
|
|
259
251
|
// ── Sessions command ─────────────────────────────────────────────────────────
|
|
@@ -381,6 +373,20 @@ else if (args[0] === "explain" && args[1]) {
|
|
|
381
373
|
console.log(explanation);
|
|
382
374
|
}
|
|
383
375
|
|
|
376
|
+
// ── Discover command ─────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
else if (args[0] === "discover") {
|
|
379
|
+
const { discover, formatDiscoverReport } = await import("./discover.js");
|
|
380
|
+
const days = parseInt(args.find(a => a.startsWith("--days="))?.split("=")[1] ?? "30");
|
|
381
|
+
const json = args.includes("--json");
|
|
382
|
+
const report = discover({ maxAgeDays: days });
|
|
383
|
+
if (json) {
|
|
384
|
+
console.log(JSON.stringify(report, null, 2));
|
|
385
|
+
} else {
|
|
386
|
+
console.log(formatDiscoverReport(report));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
384
390
|
// ── Snapshot command ─────────────────────────────────────────────────────────
|
|
385
391
|
|
|
386
392
|
else if (args[0] === "snapshot") {
|
|
@@ -411,6 +417,7 @@ else if (args.length > 0) {
|
|
|
411
417
|
const { processOutput, shouldProcess } = await import("./output-processor.js");
|
|
412
418
|
const { rewriteCommand } = await import("./command-rewriter.js");
|
|
413
419
|
const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
|
|
420
|
+
const { saveOutput, formatOutputHint } = await import("./output-store.js");
|
|
414
421
|
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
415
422
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
416
423
|
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
@@ -418,6 +425,7 @@ else if (args.length > 0) {
|
|
|
418
425
|
const { loadConfig } = await import("./history.js");
|
|
419
426
|
const { loadContext, saveContext, formatContext } = await import("./session-context.js");
|
|
420
427
|
const { getLearned, recordMapping } = await import("./usage-cache.js");
|
|
428
|
+
const { recordCorrection, findSimilarCorrections, recordOutput } = await import("./sessions-db.js");
|
|
421
429
|
|
|
422
430
|
const config = loadConfig();
|
|
423
431
|
const perms = config.permissions;
|
|
@@ -566,7 +574,14 @@ else if (args.length > 0) {
|
|
|
566
574
|
const processed = await processOutput(actualCmd, clean, prompt);
|
|
567
575
|
if (processed.aiProcessed) {
|
|
568
576
|
if (processed.tokensSaved > 0) recordSaving("compressed", processed.tokensSaved);
|
|
569
|
-
|
|
577
|
+
// Save full output for lazy recovery — agents can read the file
|
|
578
|
+
if (processed.tokensSaved > 50) {
|
|
579
|
+
const outputPath = saveOutput(actualCmd, clean);
|
|
580
|
+
console.log(processed.summary);
|
|
581
|
+
console.log(formatOutputHint(outputPath));
|
|
582
|
+
} else {
|
|
583
|
+
console.log(processed.summary);
|
|
584
|
+
}
|
|
570
585
|
if (processed.tokensSaved > 10) console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
|
|
571
586
|
process.exit(0);
|
|
572
587
|
}
|
|
@@ -625,6 +640,8 @@ else if (args.length > 0) {
|
|
|
625
640
|
const retryResult = execSync(retryCmd + " #(retry)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
626
641
|
const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
|
|
627
642
|
if (retryClean.length > 5) {
|
|
643
|
+
// Record the correction so we learn from it
|
|
644
|
+
recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
|
|
628
645
|
const processed = await processOutput(retryCmd, retryClean, prompt);
|
|
629
646
|
console.log(processed.aiProcessed ? processed.summary : retryClean);
|
|
630
647
|
process.exit(0);
|
package/src/context-hints.ts
CHANGED
|
@@ -160,9 +160,98 @@ export function discoverOutputHints(output: string, command: string): string[] {
|
|
|
160
160
|
// Sensitive data (only env var assignments, not code containing the word KEY/TOKEN)
|
|
161
161
|
if (output.match(/^[A-Z_]+(KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S+/m)) hints.push("Output may contain sensitive data — redact credentials");
|
|
162
162
|
|
|
163
|
+
// Error block extraction — state machine that captures multi-line errors
|
|
164
|
+
if (!isGrepOutput) {
|
|
165
|
+
const errorBlocks = extractErrorBlocks(output);
|
|
166
|
+
if (errorBlocks.length > 0) {
|
|
167
|
+
const summary = errorBlocks.slice(0, 3).map(b => b.trim().split("\n").slice(0, 5).join("\n")).join("\n---\n");
|
|
168
|
+
hints.push(`ERROR BLOCKS FOUND (${errorBlocks.length}):\n${summary}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
return hints;
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
/** Extract multi-line error blocks using a state machine */
|
|
176
|
+
function extractErrorBlocks(output: string): string[] {
|
|
177
|
+
const lines = output.split("\n");
|
|
178
|
+
const blocks: string[] = [];
|
|
179
|
+
let currentBlock: string[] = [];
|
|
180
|
+
let inErrorBlock = false;
|
|
181
|
+
let blankCount = 0;
|
|
182
|
+
|
|
183
|
+
// Patterns that START an error block
|
|
184
|
+
const errorStarters = [
|
|
185
|
+
/^error/i, /^Error:/i, /^ERROR/,
|
|
186
|
+
/^Traceback/i, /^panic:/i, /^fatal:/i,
|
|
187
|
+
/^FAIL/i, /^✗/, /^✘/,
|
|
188
|
+
/error\s*TS\d+/i, /error\[E\d+\]/,
|
|
189
|
+
/^SyntaxError/i, /^TypeError/i, /^ReferenceError/i,
|
|
190
|
+
/^Unhandled/i, /^Exception/i,
|
|
191
|
+
/ENOENT|EACCES|EADDRINUSE|ECONNREFUSED/,
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
const trimmed = line.trim();
|
|
196
|
+
|
|
197
|
+
if (!trimmed) {
|
|
198
|
+
blankCount++;
|
|
199
|
+
if (inErrorBlock) {
|
|
200
|
+
currentBlock.push(line);
|
|
201
|
+
// 2+ blank lines = end of error block
|
|
202
|
+
if (blankCount >= 2) {
|
|
203
|
+
blocks.push(currentBlock.join("\n").trim());
|
|
204
|
+
currentBlock = [];
|
|
205
|
+
inErrorBlock = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
blankCount = 0;
|
|
211
|
+
|
|
212
|
+
// Check if this line starts a new error block
|
|
213
|
+
if (!inErrorBlock && errorStarters.some(p => p.test(trimmed))) {
|
|
214
|
+
inErrorBlock = true;
|
|
215
|
+
currentBlock = [line];
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (inErrorBlock) {
|
|
220
|
+
// Continuation: indented lines, "at ..." stack frames, "--->" pointers, "File ..." python traces
|
|
221
|
+
const isContinuation =
|
|
222
|
+
/^\s+/.test(line) ||
|
|
223
|
+
/^\s*at\s/.test(trimmed) ||
|
|
224
|
+
/^\s*-+>/.test(trimmed) ||
|
|
225
|
+
/^\s*\|/.test(trimmed) ||
|
|
226
|
+
/^\s*File "/.test(trimmed) ||
|
|
227
|
+
/^\s*\d+\s*\|/.test(trimmed) || // rust/compiler line numbers
|
|
228
|
+
/^Caused by:/i.test(trimmed);
|
|
229
|
+
|
|
230
|
+
if (isContinuation) {
|
|
231
|
+
currentBlock.push(line);
|
|
232
|
+
} else {
|
|
233
|
+
// Non-continuation, non-blank = end of error block
|
|
234
|
+
blocks.push(currentBlock.join("\n").trim());
|
|
235
|
+
currentBlock = [];
|
|
236
|
+
inErrorBlock = false;
|
|
237
|
+
|
|
238
|
+
// Check if THIS line starts a new error block
|
|
239
|
+
if (errorStarters.some(p => p.test(trimmed))) {
|
|
240
|
+
inErrorBlock = true;
|
|
241
|
+
currentBlock = [line];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Flush remaining block
|
|
248
|
+
if (currentBlock.length > 0) {
|
|
249
|
+
blocks.push(currentBlock.join("\n").trim());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return blocks;
|
|
253
|
+
}
|
|
254
|
+
|
|
166
255
|
/** Discover safety hints about a command */
|
|
167
256
|
export function discoverSafetyHints(command: string): string[] {
|
|
168
257
|
const hints: string[] = [];
|
package/src/discover.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Discover — scan Claude Code session history to find token savings opportunities
|
|
2
|
+
// Reads ~/.claude/projects/*/sessions/*.jsonl, extracts Bash commands + output sizes,
|
|
3
|
+
// estimates how much terminal would have saved.
|
|
4
|
+
|
|
5
|
+
import { readdirSync, readFileSync, statSync, existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { estimateTokens } from "./parsers/index.js";
|
|
8
|
+
|
|
9
|
+
export interface DiscoveredCommand {
|
|
10
|
+
command: string;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
outputChars: number;
|
|
13
|
+
sessionFile: string;
|
|
14
|
+
timestamp?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoverReport {
|
|
18
|
+
totalSessions: number;
|
|
19
|
+
totalCommands: number;
|
|
20
|
+
totalOutputTokens: number;
|
|
21
|
+
estimatedSavings: number; // tokens saved at 70% compression
|
|
22
|
+
estimatedSavingsUsd: number; // at Opus rates ($5/M input)
|
|
23
|
+
topCommands: { command: string; count: number; totalTokens: number; avgTokens: number }[];
|
|
24
|
+
commandsByCategory: Record<string, { count: number; tokens: number }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Find all Claude session JSONL files */
|
|
28
|
+
function findSessionFiles(claudeDir: string, maxAge?: number): string[] {
|
|
29
|
+
const files: string[] = [];
|
|
30
|
+
const projectsDir = join(claudeDir, "projects");
|
|
31
|
+
if (!existsSync(projectsDir)) return files;
|
|
32
|
+
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const cutoff = maxAge ? now - maxAge : 0;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
for (const project of readdirSync(projectsDir)) {
|
|
38
|
+
const projectPath = join(projectsDir, project);
|
|
39
|
+
// Look for session JSONL files (not subagents)
|
|
40
|
+
try {
|
|
41
|
+
for (const entry of readdirSync(projectPath)) {
|
|
42
|
+
if (entry.endsWith(".jsonl")) {
|
|
43
|
+
const filePath = join(projectPath, entry);
|
|
44
|
+
try {
|
|
45
|
+
const stat = statSync(filePath);
|
|
46
|
+
if (stat.mtimeMs > cutoff) files.push(filePath);
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Extract Bash commands and their output sizes from a session file */
|
|
58
|
+
function extractCommands(sessionFile: string): DiscoveredCommand[] {
|
|
59
|
+
const commands: DiscoveredCommand[] = [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const content = readFileSync(sessionFile, "utf8");
|
|
63
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
64
|
+
|
|
65
|
+
// Track tool_use IDs to match with tool_results
|
|
66
|
+
const pendingToolUses: Map<string, string> = new Map(); // id -> command
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
try {
|
|
70
|
+
const obj = JSON.parse(line);
|
|
71
|
+
const msg = obj.message;
|
|
72
|
+
if (!msg?.content || !Array.isArray(msg.content)) continue;
|
|
73
|
+
|
|
74
|
+
for (const block of msg.content) {
|
|
75
|
+
// Capture Bash tool_use commands
|
|
76
|
+
if (block.type === "tool_use" && block.name === "Bash" && block.input?.command) {
|
|
77
|
+
pendingToolUses.set(block.id, block.input.command);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Capture tool_result outputs and match to commands
|
|
81
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
82
|
+
const command = pendingToolUses.get(block.tool_use_id);
|
|
83
|
+
if (command) {
|
|
84
|
+
let outputText = "";
|
|
85
|
+
if (typeof block.content === "string") {
|
|
86
|
+
outputText = block.content;
|
|
87
|
+
} else if (Array.isArray(block.content)) {
|
|
88
|
+
outputText = block.content
|
|
89
|
+
.filter((c: any) => c.type === "text")
|
|
90
|
+
.map((c: any) => c.text)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (outputText.length > 0) {
|
|
95
|
+
commands.push({
|
|
96
|
+
command,
|
|
97
|
+
outputTokens: estimateTokens(outputText),
|
|
98
|
+
outputChars: outputText.length,
|
|
99
|
+
sessionFile,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
pendingToolUses.delete(block.tool_use_id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {} // skip malformed lines
|
|
107
|
+
}
|
|
108
|
+
} catch {} // skip unreadable files
|
|
109
|
+
|
|
110
|
+
return commands;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Categorize a command into a bucket */
|
|
114
|
+
function categorizeCommand(cmd: string): string {
|
|
115
|
+
const trimmed = cmd.trim();
|
|
116
|
+
if (/^git\b/.test(trimmed)) return "git";
|
|
117
|
+
if (/\b(bun|npm|yarn|pnpm)\s+(test|run\s+test)/.test(trimmed)) return "test";
|
|
118
|
+
if (/\b(bun|npm|yarn|pnpm)\s+run\s+(build|typecheck|lint)/.test(trimmed)) return "build";
|
|
119
|
+
if (/^(grep|rg)\b/.test(trimmed)) return "grep";
|
|
120
|
+
if (/^find\b/.test(trimmed)) return "find";
|
|
121
|
+
if (/^(cat|head|tail|less)\b/.test(trimmed)) return "read";
|
|
122
|
+
if (/^(ls|tree|du|wc)\b/.test(trimmed)) return "list";
|
|
123
|
+
if (/^(curl|wget|fetch)\b/.test(trimmed)) return "network";
|
|
124
|
+
if (/^(docker|kubectl|helm)\b/.test(trimmed)) return "infra";
|
|
125
|
+
if (/^(python|pip|pytest)\b/.test(trimmed)) return "python";
|
|
126
|
+
if (/^(cargo|rustc)\b/.test(trimmed)) return "rust";
|
|
127
|
+
if (/^(go\s|golangci)\b/.test(trimmed)) return "go";
|
|
128
|
+
return "other";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Normalize command for grouping (strip variable parts like paths, hashes) */
|
|
132
|
+
function normalizeCommand(cmd: string): string {
|
|
133
|
+
return cmd
|
|
134
|
+
.replace(/[0-9a-f]{7,40}/g, "{hash}") // git hashes
|
|
135
|
+
.replace(/\/[\w./-]+\.(ts|tsx|js|json|py|rs|go)\b/g, "{file}") // file paths
|
|
136
|
+
.replace(/\d{4}-\d{2}-\d{2}/g, "{date}") // dates
|
|
137
|
+
.replace(/:\d+/g, ":{line}") // line numbers
|
|
138
|
+
.trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Run discover across all Claude sessions */
|
|
142
|
+
export function discover(options: { maxAgeDays?: number; minTokens?: number } = {}): DiscoverReport {
|
|
143
|
+
const claudeDir = join(process.env.HOME ?? "~", ".claude");
|
|
144
|
+
const maxAge = (options.maxAgeDays ?? 30) * 24 * 60 * 60 * 1000;
|
|
145
|
+
const minTokens = options.minTokens ?? 50;
|
|
146
|
+
|
|
147
|
+
const sessionFiles = findSessionFiles(claudeDir, maxAge);
|
|
148
|
+
const allCommands: DiscoveredCommand[] = [];
|
|
149
|
+
|
|
150
|
+
for (const file of sessionFiles) {
|
|
151
|
+
allCommands.push(...extractCommands(file));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Filter to commands with meaningful output
|
|
155
|
+
const significant = allCommands.filter(c => c.outputTokens >= minTokens);
|
|
156
|
+
|
|
157
|
+
// Group by normalized command
|
|
158
|
+
const groups = new Map<string, { count: number; totalTokens: number; example: string }>();
|
|
159
|
+
for (const cmd of significant) {
|
|
160
|
+
const key = normalizeCommand(cmd.command);
|
|
161
|
+
const existing = groups.get(key) ?? { count: 0, totalTokens: 0, example: cmd.command };
|
|
162
|
+
existing.count++;
|
|
163
|
+
existing.totalTokens += cmd.outputTokens;
|
|
164
|
+
groups.set(key, existing);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Top commands by total tokens
|
|
168
|
+
const topCommands = [...groups.entries()]
|
|
169
|
+
.map(([cmd, data]) => ({
|
|
170
|
+
command: data.example,
|
|
171
|
+
count: data.count,
|
|
172
|
+
totalTokens: data.totalTokens,
|
|
173
|
+
avgTokens: Math.round(data.totalTokens / data.count),
|
|
174
|
+
}))
|
|
175
|
+
.sort((a, b) => b.totalTokens - a.totalTokens)
|
|
176
|
+
.slice(0, 20);
|
|
177
|
+
|
|
178
|
+
// Category breakdown
|
|
179
|
+
const commandsByCategory: Record<string, { count: number; tokens: number }> = {};
|
|
180
|
+
for (const cmd of significant) {
|
|
181
|
+
const cat = categorizeCommand(cmd.command);
|
|
182
|
+
if (!commandsByCategory[cat]) commandsByCategory[cat] = { count: 0, tokens: 0 };
|
|
183
|
+
commandsByCategory[cat].count++;
|
|
184
|
+
commandsByCategory[cat].tokens += cmd.outputTokens;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const totalOutputTokens = significant.reduce((sum, c) => sum + c.outputTokens, 0);
|
|
188
|
+
// Conservative 70% compression estimate (RTK claims 60-90%)
|
|
189
|
+
const estimatedSavings = Math.round(totalOutputTokens * 0.7);
|
|
190
|
+
// Each saved input token is repeated across ~5 turns on average before compaction
|
|
191
|
+
const multipliedSavings = estimatedSavings * 5;
|
|
192
|
+
// At Opus rates ($5/M input tokens)
|
|
193
|
+
const estimatedSavingsUsd = (multipliedSavings * 5) / 1_000_000;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
totalSessions: sessionFiles.length,
|
|
197
|
+
totalCommands: significant.length,
|
|
198
|
+
totalOutputTokens,
|
|
199
|
+
estimatedSavings,
|
|
200
|
+
estimatedSavingsUsd,
|
|
201
|
+
topCommands,
|
|
202
|
+
commandsByCategory,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Format discover report for CLI display */
|
|
207
|
+
export function formatDiscoverReport(report: DiscoverReport): string {
|
|
208
|
+
const lines: string[] = [];
|
|
209
|
+
|
|
210
|
+
lines.push(`📊 Terminal Discover — Token Savings Analysis`);
|
|
211
|
+
lines.push(` Scanned ${report.totalSessions} sessions, ${report.totalCommands} commands with >50 token output\n`);
|
|
212
|
+
|
|
213
|
+
lines.push(`💰 Estimated savings with open-terminal:`);
|
|
214
|
+
lines.push(` Output tokens: ${report.totalOutputTokens.toLocaleString()}`);
|
|
215
|
+
lines.push(` Compressible: ${report.estimatedSavings.toLocaleString()} tokens (70% avg)`);
|
|
216
|
+
lines.push(` Repeated ~5x before compaction = ${(report.estimatedSavings * 5).toLocaleString()} billable tokens`);
|
|
217
|
+
lines.push(` At Opus rates: $${report.estimatedSavingsUsd.toFixed(2)} saved\n`);
|
|
218
|
+
|
|
219
|
+
if (report.topCommands.length > 0) {
|
|
220
|
+
lines.push(`🔝 Top commands by token cost:`);
|
|
221
|
+
for (const cmd of report.topCommands.slice(0, 15)) {
|
|
222
|
+
const avg = cmd.avgTokens.toLocaleString().padStart(6);
|
|
223
|
+
const total = cmd.totalTokens.toLocaleString().padStart(8);
|
|
224
|
+
lines.push(` ${String(cmd.count).padStart(4)}× ${avg} avg → ${total} total ${cmd.command.slice(0, 60)}`);
|
|
225
|
+
}
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (Object.keys(report.commandsByCategory).length > 0) {
|
|
230
|
+
lines.push(`📁 By category:`);
|
|
231
|
+
const sorted = Object.entries(report.commandsByCategory).sort((a, b) => b[1].tokens - a[1].tokens);
|
|
232
|
+
for (const [cat, data] of sorted) {
|
|
233
|
+
lines.push(` ${cat.padEnd(10)} ${String(data.count).padStart(5)} cmds ${data.tokens.toLocaleString().padStart(10)} tokens`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|