@hasna/terminal 2.0.5 → 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 +52 -21
- package/package.json +1 -1
- package/src/ai.ts +77 -130
- package/src/cli.tsx +51 -21
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +291 -0
- package/src/discover.ts +238 -0
- package/src/economy.ts +53 -0
- package/src/output-processor.ts +7 -18
- package/src/output-store.ts +65 -0
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- 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/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
package/src/command-validator.ts
CHANGED
|
@@ -78,6 +78,17 @@ export function validateCommand(command: string, cwd: string): ValidationResult
|
|
|
78
78
|
issues.push(`GNU flag on macOS: ${gnuFlags.join(", ")}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Complexity guard — extreme pipe chains are fragile
|
|
82
|
+
const pipeCount = (command.match(/\|/g) || []).length;
|
|
83
|
+
if (pipeCount > 7) {
|
|
84
|
+
issues.push(`too complex: ${pipeCount} pipes — simplify`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// grep -P (PCRE) doesn't exist on macOS
|
|
88
|
+
if (/grep\s+.*-[a-zA-Z]*P/.test(command)) {
|
|
89
|
+
issues.push("grep -P (PCRE) not available on macOS — use grep -E");
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
return {
|
|
82
93
|
valid: issues.length === 0,
|
|
83
94
|
issues,
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Context hints — discover context via lightweight checks, inject into AI prompt
|
|
2
|
+
// Regex DISCOVERS, AI DECIDES. No hardcoded logic that makes decisions.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
export interface ContextHints {
|
|
8
|
+
project: string[]; // project metadata
|
|
9
|
+
output: string[]; // observations about command output
|
|
10
|
+
safety: string[]; // safety-relevant observations
|
|
11
|
+
environment: string[]; // system/env observations
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Discover project context from the filesystem */
|
|
15
|
+
export function discoverProjectHints(cwd: string): string[] {
|
|
16
|
+
const hints: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Package managers and project files
|
|
19
|
+
const projectFiles: [string, string][] = [
|
|
20
|
+
["package.json", "Node.js/TypeScript"],
|
|
21
|
+
["pyproject.toml", "Python"],
|
|
22
|
+
["requirements.txt", "Python"],
|
|
23
|
+
["go.mod", "Go"],
|
|
24
|
+
["Cargo.toml", "Rust"],
|
|
25
|
+
["pom.xml", "Java/Maven"],
|
|
26
|
+
["build.gradle", "Java/Gradle"],
|
|
27
|
+
["build.gradle.kts", "Java/Gradle (Kotlin DSL)"],
|
|
28
|
+
["Makefile", "Has Makefile"],
|
|
29
|
+
["Dockerfile", "Has Docker"],
|
|
30
|
+
["docker-compose.yml", "Has Docker Compose"],
|
|
31
|
+
["docker-compose.yaml", "Has Docker Compose"],
|
|
32
|
+
[".github/workflows", "Has GitHub Actions CI"],
|
|
33
|
+
["Gemfile", "Ruby"],
|
|
34
|
+
["composer.json", "PHP"],
|
|
35
|
+
["mix.exs", "Elixir"],
|
|
36
|
+
["build.zig", "Zig"],
|
|
37
|
+
["CMakeLists.txt", "C/C++ (CMake)"],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const [file, lang] of projectFiles) {
|
|
41
|
+
if (existsSync(join(cwd, file))) {
|
|
42
|
+
hints.push(`Project type: ${lang} (${file} found)`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract rich metadata from package.json
|
|
47
|
+
const pkgPath = join(cwd, "package.json");
|
|
48
|
+
if (existsSync(pkgPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
51
|
+
if (pkg.name) hints.push(`Package name: ${pkg.name}@${pkg.version ?? "unknown"}`);
|
|
52
|
+
if (pkg.scripts) {
|
|
53
|
+
hints.push(`Available scripts: ${Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).slice(0, 10).join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
if (pkg.dependencies) hints.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract from pyproject.toml
|
|
60
|
+
const pyPath = join(cwd, "pyproject.toml");
|
|
61
|
+
if (existsSync(pyPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const py = readFileSync(pyPath, "utf8");
|
|
64
|
+
const name = py.match(/name\s*=\s*"([^"]+)"/)?.[1];
|
|
65
|
+
if (name) hints.push(`Python package: ${name}`);
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract from go.mod
|
|
70
|
+
const goPath = join(cwd, "go.mod");
|
|
71
|
+
if (existsSync(goPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const go = readFileSync(goPath, "utf8");
|
|
74
|
+
const mod = go.match(/module\s+(\S+)/)?.[1];
|
|
75
|
+
if (mod) hints.push(`Go module: ${mod}`);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract from Cargo.toml
|
|
80
|
+
const cargoPath = join(cwd, "Cargo.toml");
|
|
81
|
+
if (existsSync(cargoPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const cargo = readFileSync(cargoPath, "utf8");
|
|
84
|
+
const name = cargo.match(/name\s*=\s*"([^"]+)"/)?.[1];
|
|
85
|
+
if (name) hints.push(`Rust crate: ${name}`);
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Monorepo detection
|
|
90
|
+
if (existsSync(join(cwd, "packages"))) {
|
|
91
|
+
try {
|
|
92
|
+
const pkgs = readdirSync(join(cwd, "packages")).filter(d => !d.startsWith("."));
|
|
93
|
+
hints.push(`MONOREPO: ${pkgs.length} packages in packages/ — search packages/ not src/`);
|
|
94
|
+
hints.push(`Packages: ${pkgs.slice(0, 10).join(", ")}`);
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
if (existsSync(join(cwd, "apps"))) {
|
|
98
|
+
hints.push("MONOREPO: apps/ directory detected");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Makefile targets
|
|
102
|
+
if (existsSync(join(cwd, "Makefile"))) {
|
|
103
|
+
try {
|
|
104
|
+
const { execSync } = require("child_process");
|
|
105
|
+
const targets = execSync("grep -E '^[a-zA-Z_-]+:' Makefile | head -10 | cut -d: -f1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
106
|
+
if (targets) hints.push(`Makefile targets: ${targets.split("\n").join(", ")}`);
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Source directory structure
|
|
111
|
+
try {
|
|
112
|
+
const { execSync } = require("child_process");
|
|
113
|
+
const srcDirs = ["src", "lib", "app", "packages"];
|
|
114
|
+
for (const dir of srcDirs) {
|
|
115
|
+
if (existsSync(join(cwd, dir))) {
|
|
116
|
+
const tree = execSync(
|
|
117
|
+
`find ${dir} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' 2>/dev/null | sort | head -60`,
|
|
118
|
+
{ cwd, encoding: "utf8", timeout: 3000 }
|
|
119
|
+
).trim();
|
|
120
|
+
if (tree) hints.push(`Files in ${dir}/:\n${tree}`);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Top-level files
|
|
125
|
+
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
126
|
+
hints.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
return hints;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Discover output-specific hints (observations about command output) */
|
|
133
|
+
export function discoverOutputHints(output: string, command: string): string[] {
|
|
134
|
+
const hints: string[] = [];
|
|
135
|
+
|
|
136
|
+
const lines = output.split("\n");
|
|
137
|
+
hints.push(`Output: ${lines.length} lines, ${output.length} chars`);
|
|
138
|
+
|
|
139
|
+
// Only detect test results from actual test runners (not grep output containing "pass"/"fail" in code)
|
|
140
|
+
const isGrepOutput = /^\s*(src\/|\.\/|packages\/).*:\d+:/.test(output);
|
|
141
|
+
if (!isGrepOutput) {
|
|
142
|
+
const passMatch = output.match(/(\d+)\s+pass(?:ed|ing)?\b/i);
|
|
143
|
+
const failMatch = output.match(/(\d+)\s+fail(?:ed|ing|ure)?\b/i);
|
|
144
|
+
if (passMatch) hints.push(`Test results detected: ${passMatch[0]}`);
|
|
145
|
+
if (failMatch) hints.push(`Test results detected: ${failMatch[0]}`);
|
|
146
|
+
|
|
147
|
+
// Error patterns (only from actual command output, not code search)
|
|
148
|
+
if (output.match(/error\s*TS\d+/i)) hints.push("TypeScript errors detected in output");
|
|
149
|
+
if (output.match(/ENOENT|EACCES|EADDRINUSE/)) hints.push("System error code detected in output");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Coverage patterns
|
|
153
|
+
if (output.match(/%\s*Funcs|%\s*Lines|coverage/i)) hints.push("Code coverage data detected in output");
|
|
154
|
+
|
|
155
|
+
// Large/repetitive output
|
|
156
|
+
if (lines.length > 100) hints.push(`Large output (${lines.length} lines) — consider summarizing`);
|
|
157
|
+
const uniqueLines = new Set(lines.map(l => l.trim())).size;
|
|
158
|
+
if (uniqueLines < lines.length * 0.5) hints.push("Output has many duplicate/similar lines");
|
|
159
|
+
|
|
160
|
+
// Sensitive data (only env var assignments, not code containing the word KEY/TOKEN)
|
|
161
|
+
if (output.match(/^[A-Z_]+(KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S+/m)) hints.push("Output may contain sensitive data — redact credentials");
|
|
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
|
+
|
|
172
|
+
return hints;
|
|
173
|
+
}
|
|
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
|
+
|
|
255
|
+
/** Discover safety hints about a command */
|
|
256
|
+
export function discoverSafetyHints(command: string): string[] {
|
|
257
|
+
const hints: string[] = [];
|
|
258
|
+
|
|
259
|
+
// Observations about the command (AI decides if it's safe)
|
|
260
|
+
if (command.match(/\brm\b|\brmdir\b|\btruncate\b/)) hints.push("SAFETY: command contains file deletion (rm/rmdir/truncate)");
|
|
261
|
+
if (command.match(/\bkill\b|\bkillall\b|\bpkill\b/)) hints.push("SAFETY: command kills processes");
|
|
262
|
+
if (command.match(/\bgit\s+push\b|\bgit\s+reset\s+--hard\b/)) hints.push("SAFETY: command pushes/resets git");
|
|
263
|
+
if (command.match(/\bnpx\b|\bnpm\s+install\b|\bpip\s+install\b/)) hints.push("SAFETY: command installs packages");
|
|
264
|
+
if (command.match(/\bsed\s+-i\b|\bcodemod\b/)) hints.push("SAFETY: command modifies files in-place");
|
|
265
|
+
if (command.match(/\btouch\b|\bmkdir\b/)) hints.push("SAFETY: command creates files/directories");
|
|
266
|
+
if (command.match(/>\s*\S+\.\w+/)) hints.push("SAFETY: command writes to a file via redirect");
|
|
267
|
+
if (command.match(/\b(bun|npm|pnpm)\s+run\s+dev\b|\bstart\b/)) hints.push("SAFETY: command starts a server/process");
|
|
268
|
+
|
|
269
|
+
// Read-only observations
|
|
270
|
+
if (command.match(/^\s*git\s+(log|show|diff|status|branch|blame|tag)\b/)) hints.push("This is a read-only git command");
|
|
271
|
+
if (command.match(/^\s*(ls|cat|head|tail|grep|find|wc|du|df|uptime|whoami|pwd)\b/)) hints.push("This is a read-only command");
|
|
272
|
+
|
|
273
|
+
return hints;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Format all hints for system prompt injection */
|
|
277
|
+
export function formatHints(project: string[], output?: string[], safety?: string[]): string {
|
|
278
|
+
const sections: string[] = [];
|
|
279
|
+
|
|
280
|
+
if (project.length > 0) {
|
|
281
|
+
sections.push("PROJECT CONTEXT:\n" + project.join("\n"));
|
|
282
|
+
}
|
|
283
|
+
if (output && output.length > 0) {
|
|
284
|
+
sections.push("OUTPUT OBSERVATIONS:\n" + output.join("\n"));
|
|
285
|
+
}
|
|
286
|
+
if (safety && safety.length > 0) {
|
|
287
|
+
sections.push("SAFETY OBSERVATIONS:\n" + safety.join("\n"));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return sections.join("\n\n");
|
|
291
|
+
}
|
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
|
+
}
|
package/src/economy.ts
CHANGED
|
@@ -97,3 +97,56 @@ export function formatTokens(n: number): string {
|
|
|
97
97
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
98
98
|
return `${n}`;
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
// ── Weighted economics ──────────────────────────────────────────────────────
|
|
102
|
+
// Saved input tokens are repeated across multiple turns before compaction.
|
|
103
|
+
// Weighted pricing accounts for the actual billing impact.
|
|
104
|
+
|
|
105
|
+
/** Provider pricing per million tokens */
|
|
106
|
+
const PROVIDER_PRICING: Record<string, { input: number; output: number }> = {
|
|
107
|
+
cerebras: { input: 0.60, output: 1.20 },
|
|
108
|
+
groq: { input: 0.15, output: 0.60 },
|
|
109
|
+
xai: { input: 0.20, output: 1.50 },
|
|
110
|
+
anthropic: { input: 0.80, output: 4.00 }, // Haiku
|
|
111
|
+
"anthropic-sonnet": { input: 3.00, output: 15.00 },
|
|
112
|
+
"anthropic-opus": { input: 5.00, output: 25.00 },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Estimate USD savings from compressed tokens */
|
|
116
|
+
export function estimateSavingsUsd(
|
|
117
|
+
tokensSaved: number,
|
|
118
|
+
consumerModel: string = "anthropic-opus",
|
|
119
|
+
avgTurnsBeforeCompaction: number = 5,
|
|
120
|
+
): { savingsUsd: number; multipliedTokens: number; ratePerMillion: number } {
|
|
121
|
+
const pricing = PROVIDER_PRICING[consumerModel] ?? PROVIDER_PRICING["anthropic-opus"];
|
|
122
|
+
const multipliedTokens = tokensSaved * avgTurnsBeforeCompaction;
|
|
123
|
+
const savingsUsd = (multipliedTokens * pricing.input) / 1_000_000;
|
|
124
|
+
return { savingsUsd, multipliedTokens, ratePerMillion: pricing.input };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Format a full economics summary */
|
|
128
|
+
export function formatEconomicsSummary(): string {
|
|
129
|
+
const s = loadStats();
|
|
130
|
+
const opus = estimateSavingsUsd(s.totalTokensSaved, "anthropic-opus");
|
|
131
|
+
const sonnet = estimateSavingsUsd(s.totalTokensSaved, "anthropic-sonnet");
|
|
132
|
+
const haiku = estimateSavingsUsd(s.totalTokensSaved, "anthropic");
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
`Token Economy:`,
|
|
136
|
+
` Tokens saved: ${formatTokens(s.totalTokensSaved)}`,
|
|
137
|
+
` Tokens used: ${formatTokens(s.totalTokensUsed)}`,
|
|
138
|
+
` Ratio: ${s.totalTokensUsed > 0 ? (s.totalTokensSaved / s.totalTokensUsed).toFixed(1) : "∞"}x return`,
|
|
139
|
+
``,
|
|
140
|
+
` Estimated USD savings (×5 turns before compaction):`,
|
|
141
|
+
` Opus ($5/M): $${opus.savingsUsd.toFixed(2)} (${formatTokens(opus.multipliedTokens)} billable tokens)`,
|
|
142
|
+
` Sonnet ($3/M): $${sonnet.savingsUsd.toFixed(2)}`,
|
|
143
|
+
` Haiku ($0.8/M): $${haiku.savingsUsd.toFixed(2)}`,
|
|
144
|
+
``,
|
|
145
|
+
` By feature:`,
|
|
146
|
+
` Compressed: ${formatTokens(s.savingsByFeature.compressed)}`,
|
|
147
|
+
` Structured: ${formatTokens(s.savingsByFeature.structured)}`,
|
|
148
|
+
` Diff cache: ${formatTokens(s.savingsByFeature.diff)}`,
|
|
149
|
+
` NL cache: ${formatTokens(s.savingsByFeature.cache)}`,
|
|
150
|
+
` Search: ${formatTokens(s.savingsByFeature.search)}`,
|
|
151
|
+
].join("\n");
|
|
152
|
+
}
|
package/src/output-processor.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { getProvider } from "./providers/index.js";
|
|
5
5
|
import { estimateTokens } from "./parsers/index.js";
|
|
6
6
|
import { recordSaving } from "./economy.js";
|
|
7
|
+
import { discoverOutputHints } from "./context-hints.js";
|
|
7
8
|
|
|
8
9
|
export interface ProcessedOutput {
|
|
9
10
|
/** AI-generated summary (concise, structured) */
|
|
@@ -79,27 +80,15 @@ export async function processOutput(
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
try {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (passMatch && failMatch && originalPrompt && /test|pass|fail/i.test(originalPrompt)) {
|
|
88
|
-
const passed = parseInt(passMatch[1]);
|
|
89
|
-
const failed = parseInt(failMatch[1]);
|
|
90
|
-
const answer = failed === 0
|
|
91
|
-
? `✓ Yes, all ${passed} tests pass.`
|
|
92
|
-
: `✗ ${failed} of ${passed + failed} tests failed.`;
|
|
93
|
-
const savedTokens = estimateTokens(output) - estimateTokens(answer);
|
|
94
|
-
return {
|
|
95
|
-
summary: answer, full: output, tokensSaved: Math.max(0, savedTokens),
|
|
96
|
-
aiTokensUsed: 0, aiProcessed: true, aiCostUsd: 0, savingsValueUsd: 0, netSavingsUsd: 0,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
83
|
+
// Discover output hints — regex discovers patterns, AI decides what matters
|
|
84
|
+
const outputHints = discoverOutputHints(output, command);
|
|
85
|
+
const hintsBlock = outputHints.length > 0
|
|
86
|
+
? `\n\nOUTPUT OBSERVATIONS:\n${outputHints.join("\n")}`
|
|
87
|
+
: "";
|
|
99
88
|
|
|
100
89
|
const provider = getProvider();
|
|
101
90
|
const summary = await provider.complete(
|
|
102
|
-
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`,
|
|
91
|
+
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}`,
|
|
103
92
|
{
|
|
104
93
|
system: SUMMARIZE_PROMPT,
|
|
105
94
|
maxTokens: 300,
|