@hasna/terminal 2.3.0 → 2.3.1
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 +64 -16
- package/package.json +1 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
package/dist/cli.js
CHANGED
|
@@ -361,17 +361,52 @@ else if (args[0] === "repo") {
|
|
|
361
361
|
else if (args[0] === "symbols" && args[1]) {
|
|
362
362
|
const { extractSymbolsFromFile } = await import("./search/semantic.js");
|
|
363
363
|
const { resolve } = await import("path");
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
const { statSync, readdirSync } = await import("fs");
|
|
365
|
+
const target = resolve(args[1]);
|
|
366
|
+
const filter = args[2]; // optional: grep-like filter on symbol name
|
|
367
|
+
// Support directories — recurse and extract symbols from all source files
|
|
368
|
+
const files = [];
|
|
369
|
+
try {
|
|
370
|
+
if (statSync(target).isDirectory()) {
|
|
371
|
+
const walk = (dir) => {
|
|
372
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
373
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
374
|
+
continue;
|
|
375
|
+
const full = resolve(dir, entry.name);
|
|
376
|
+
if (entry.isDirectory())
|
|
377
|
+
walk(full);
|
|
378
|
+
else if (/\.(ts|tsx|py|go|rs)$/.test(entry.name) && !/\.(test|spec)\.\w+$/.test(entry.name))
|
|
379
|
+
files.push(full);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
walk(target);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
files.push(target);
|
|
386
|
+
}
|
|
368
387
|
}
|
|
369
|
-
|
|
370
|
-
|
|
388
|
+
catch {
|
|
389
|
+
files.push(target);
|
|
390
|
+
}
|
|
391
|
+
let totalSymbols = 0;
|
|
392
|
+
for (const file of files) {
|
|
393
|
+
const symbols = extractSymbolsFromFile(file);
|
|
394
|
+
const filtered = filter ? symbols.filter(s => s.name.toLowerCase().includes(filter.toLowerCase()) || s.kind.toLowerCase().includes(filter.toLowerCase())) : symbols;
|
|
395
|
+
if (filtered.length === 0)
|
|
396
|
+
continue;
|
|
397
|
+
const relPath = file.replace(process.cwd() + "/", "");
|
|
398
|
+
if (files.length > 1)
|
|
399
|
+
console.log(`\n${relPath}:`);
|
|
400
|
+
for (const s of filtered) {
|
|
371
401
|
const exp = s.exported ? "⬡" : "·";
|
|
372
402
|
console.log(` ${exp} ${s.kind.padEnd(10)} L${String(s.line).padStart(4)} ${s.name}`);
|
|
373
403
|
}
|
|
404
|
+
totalSymbols += filtered.length;
|
|
374
405
|
}
|
|
406
|
+
if (totalSymbols === 0)
|
|
407
|
+
console.log("No symbols found.");
|
|
408
|
+
else if (files.length > 1)
|
|
409
|
+
console.log(`\n${totalSymbols} symbols across ${files.length} files`);
|
|
375
410
|
}
|
|
376
411
|
// ── History command ──────────────────────────────────────────────────────────
|
|
377
412
|
else if (args[0] === "history") {
|
|
@@ -621,7 +656,7 @@ else if (args.length > 0) {
|
|
|
621
656
|
catch (e) {
|
|
622
657
|
// Empty result (grep exit 1 = no matches) — not a real error
|
|
623
658
|
const errStdout = e.stdout?.toString() ?? "";
|
|
624
|
-
|
|
659
|
+
let errStderr = e.stderr?.toString() ?? "";
|
|
625
660
|
if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
|
|
626
661
|
// Empty result — retry with broader scope before giving up
|
|
627
662
|
if (!actualCmd.includes("#(broadened)")) {
|
|
@@ -643,24 +678,37 @@ else if (args.length > 0) {
|
|
|
643
678
|
console.log(`No results found for: ${prompt}`);
|
|
644
679
|
process.exit(0);
|
|
645
680
|
}
|
|
646
|
-
//
|
|
647
|
-
if (e.status >= 2
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
681
|
+
// 3-retry learning loop: each attempt learns from the previous failure
|
|
682
|
+
if (e.status >= 2) {
|
|
683
|
+
const retryStrategies = [
|
|
684
|
+
// Attempt 2: inject error context
|
|
685
|
+
`${prompt} (Command "${actualCmd}" failed with: ${errStderr.slice(0, 300)}. Fix this specific error. Keep the approach but correct the issue.)`,
|
|
686
|
+
// Attempt 3: inject corrections + force simplicity
|
|
687
|
+
`${prompt} (TWO commands already failed for this query. Use the ABSOLUTE SIMPLEST approach: basic grep -rn, find, wc -l, cat. No awk, no xargs, no subshells. Must work on macOS BSD.)`,
|
|
688
|
+
];
|
|
689
|
+
for (let attempt = 0; attempt < retryStrategies.length; attempt++) {
|
|
690
|
+
try {
|
|
691
|
+
const retryCmd = await translateToCommand(retryStrategies[attempt], perms, []);
|
|
692
|
+
if (!retryCmd || retryCmd === actualCmd || isIrreversible(retryCmd) || checkPermissions(retryCmd, perms))
|
|
693
|
+
continue;
|
|
694
|
+
console.error(`[open-terminal] retry ${attempt + 2}/3: $ ${retryCmd}`);
|
|
695
|
+
const retryResult = execSync(retryCmd + ` #(retry${attempt + 2})`, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
653
696
|
const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
|
|
654
697
|
if (retryClean.length > 5) {
|
|
655
|
-
// Record
|
|
698
|
+
// Record correction — AI learns for next time
|
|
656
699
|
recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
|
|
657
700
|
const processed = await processOutput(retryCmd, retryClean, prompt);
|
|
658
701
|
console.log(processed.aiProcessed ? processed.summary : retryClean);
|
|
659
702
|
process.exit(0);
|
|
660
703
|
}
|
|
661
704
|
}
|
|
705
|
+
catch (retryErr) {
|
|
706
|
+
// This attempt also failed — record it and try next strategy
|
|
707
|
+
const retryStderr = retryErr.stderr?.toString() ?? "";
|
|
708
|
+
errStderr = retryStderr; // update for next attempt's context
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
662
711
|
}
|
|
663
|
-
catch { /* retry also failed, fall through */ }
|
|
664
712
|
}
|
|
665
713
|
// Combine stdout+stderr and try AI answer framing (for audit/lint/test commands)
|
|
666
714
|
const combined = errStderr && errStdout.includes(errStderr.trim()) ? errStdout : errStdout + errStderr;
|
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -216,6 +216,14 @@ RULES:
|
|
|
216
216
|
- For conceptual questions about what code does: use cat on the relevant file, the AI summary will explain it.
|
|
217
217
|
- For DESTRUCTIVE requests (delete, remove, install, push): output BLOCKED: <reason>. NEVER try to execute destructive commands.
|
|
218
218
|
|
|
219
|
+
AST-POWERED QUERIES: For code STRUCTURE questions, use the built-in AST tool instead of grep:
|
|
220
|
+
- "find all exported functions" → terminal symbols src/ (lists all functions, classes, interfaces with line numbers)
|
|
221
|
+
- "show all interfaces" → terminal symbols src/ | grep interface
|
|
222
|
+
- "what does file X export" → terminal symbols src/file.ts
|
|
223
|
+
- "show me the class hierarchy" → terminal symbols src/
|
|
224
|
+
The "terminal symbols" command uses AST parsing (not regex) — it understands TypeScript, Python, Go, Rust code structure.
|
|
225
|
+
For TEXT search (TODO, string matches, imports) → use grep as normal.
|
|
226
|
+
|
|
219
227
|
COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
|
|
220
228
|
- "how many tests and do they pass" → bun test (extract count AND pass/fail from output)
|
|
221
229
|
- "what files changed and how many lines" → git log --stat -3 (shows files AND line counts)
|
package/src/cli.tsx
CHANGED
|
@@ -335,15 +335,43 @@ else if (args[0] === "repo") {
|
|
|
335
335
|
else if (args[0] === "symbols" && args[1]) {
|
|
336
336
|
const { extractSymbolsFromFile } = await import("./search/semantic.js");
|
|
337
337
|
const { resolve } = await import("path");
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
338
|
+
const { statSync, readdirSync } = await import("fs");
|
|
339
|
+
const target = resolve(args[1]);
|
|
340
|
+
const filter = args[2]; // optional: grep-like filter on symbol name
|
|
341
|
+
|
|
342
|
+
// Support directories — recurse and extract symbols from all source files
|
|
343
|
+
const files: string[] = [];
|
|
344
|
+
try {
|
|
345
|
+
if (statSync(target).isDirectory()) {
|
|
346
|
+
const walk = (dir: string) => {
|
|
347
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
348
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
349
|
+
const full = resolve(dir, entry.name);
|
|
350
|
+
if (entry.isDirectory()) walk(full);
|
|
351
|
+
else if (/\.(ts|tsx|py|go|rs)$/.test(entry.name) && !/\.(test|spec)\.\w+$/.test(entry.name)) files.push(full);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
walk(target);
|
|
355
|
+
} else {
|
|
356
|
+
files.push(target);
|
|
357
|
+
}
|
|
358
|
+
} catch { files.push(target); }
|
|
359
|
+
|
|
360
|
+
let totalSymbols = 0;
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
const symbols = extractSymbolsFromFile(file);
|
|
363
|
+
const filtered = filter ? symbols.filter(s => s.name.toLowerCase().includes(filter.toLowerCase()) || s.kind.toLowerCase().includes(filter.toLowerCase())) : symbols;
|
|
364
|
+
if (filtered.length === 0) continue;
|
|
365
|
+
const relPath = file.replace(process.cwd() + "/", "");
|
|
366
|
+
if (files.length > 1) console.log(`\n${relPath}:`);
|
|
367
|
+
for (const s of filtered) {
|
|
343
368
|
const exp = s.exported ? "⬡" : "·";
|
|
344
369
|
console.log(` ${exp} ${s.kind.padEnd(10)} L${String(s.line).padStart(4)} ${s.name}`);
|
|
345
370
|
}
|
|
371
|
+
totalSymbols += filtered.length;
|
|
346
372
|
}
|
|
373
|
+
if (totalSymbols === 0) console.log("No symbols found.");
|
|
374
|
+
else if (files.length > 1) console.log(`\n${totalSymbols} symbols across ${files.length} files`);
|
|
347
375
|
}
|
|
348
376
|
|
|
349
377
|
// ── History command ──────────────────────────────────────────────────────────
|
|
@@ -603,7 +631,7 @@ else if (args.length > 0) {
|
|
|
603
631
|
} catch (e: any) {
|
|
604
632
|
// Empty result (grep exit 1 = no matches) — not a real error
|
|
605
633
|
const errStdout = e.stdout?.toString() ?? "";
|
|
606
|
-
|
|
634
|
+
let errStderr = e.stderr?.toString() ?? "";
|
|
607
635
|
if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
|
|
608
636
|
// Empty result — retry with broader scope before giving up
|
|
609
637
|
if (!actualCmd.includes("#(broadened)")) {
|
|
@@ -628,26 +656,37 @@ else if (args.length > 0) {
|
|
|
628
656
|
process.exit(0);
|
|
629
657
|
}
|
|
630
658
|
|
|
631
|
-
//
|
|
632
|
-
if (e.status >= 2
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
659
|
+
// 3-retry learning loop: each attempt learns from the previous failure
|
|
660
|
+
if (e.status >= 2) {
|
|
661
|
+
const retryStrategies = [
|
|
662
|
+
// Attempt 2: inject error context
|
|
663
|
+
`${prompt} (Command "${actualCmd}" failed with: ${errStderr.slice(0, 300)}. Fix this specific error. Keep the approach but correct the issue.)`,
|
|
664
|
+
// Attempt 3: inject corrections + force simplicity
|
|
665
|
+
`${prompt} (TWO commands already failed for this query. Use the ABSOLUTE SIMPLEST approach: basic grep -rn, find, wc -l, cat. No awk, no xargs, no subshells. Must work on macOS BSD.)`,
|
|
666
|
+
];
|
|
667
|
+
|
|
668
|
+
for (let attempt = 0; attempt < retryStrategies.length; attempt++) {
|
|
669
|
+
try {
|
|
670
|
+
const retryCmd = await translateToCommand(retryStrategies[attempt], perms, []);
|
|
671
|
+
if (!retryCmd || retryCmd === actualCmd || isIrreversible(retryCmd) || checkPermissions(retryCmd, perms)) continue;
|
|
672
|
+
|
|
673
|
+
console.error(`[open-terminal] retry ${attempt + 2}/3: $ ${retryCmd}`);
|
|
674
|
+
const retryResult = execSync(retryCmd + ` #(retry${attempt + 2})`, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
|
|
641
675
|
const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
|
|
642
676
|
if (retryClean.length > 5) {
|
|
643
|
-
// Record
|
|
677
|
+
// Record correction — AI learns for next time
|
|
644
678
|
recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
|
|
645
679
|
const processed = await processOutput(retryCmd, retryClean, prompt);
|
|
646
680
|
console.log(processed.aiProcessed ? processed.summary : retryClean);
|
|
647
681
|
process.exit(0);
|
|
648
682
|
}
|
|
683
|
+
} catch (retryErr: any) {
|
|
684
|
+
// This attempt also failed — record it and try next strategy
|
|
685
|
+
const retryStderr = retryErr.stderr?.toString() ?? "";
|
|
686
|
+
errStderr = retryStderr; // update for next attempt's context
|
|
687
|
+
continue;
|
|
649
688
|
}
|
|
650
|
-
}
|
|
689
|
+
}
|
|
651
690
|
}
|
|
652
691
|
|
|
653
692
|
// Combine stdout+stderr and try AI answer framing (for audit/lint/test commands)
|
package/src/output-processor.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getProvider } from "./providers/index.js";
|
|
|
5
5
|
import { estimateTokens } from "./parsers/index.js";
|
|
6
6
|
import { recordSaving } from "./economy.js";
|
|
7
7
|
import { discoverOutputHints } from "./context-hints.js";
|
|
8
|
+
import { formatProfileHints } from "./tool-profiles.js";
|
|
8
9
|
|
|
9
10
|
export interface ProcessedOutput {
|
|
10
11
|
/** AI-generated summary (concise, structured) */
|
|
@@ -86,9 +87,13 @@ export async function processOutput(
|
|
|
86
87
|
? `\n\nOUTPUT OBSERVATIONS:\n${outputHints.join("\n")}`
|
|
87
88
|
: "";
|
|
88
89
|
|
|
90
|
+
// Inject tool-specific profile hints
|
|
91
|
+
const profileBlock = formatProfileHints(command);
|
|
92
|
+
const profileHints = profileBlock ? `\n\n${profileBlock}` : "";
|
|
93
|
+
|
|
89
94
|
const provider = getProvider();
|
|
90
95
|
const summary = await provider.complete(
|
|
91
|
-
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}`,
|
|
96
|
+
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}${profileHints}`,
|
|
92
97
|
{
|
|
93
98
|
system: SUMMARIZE_PROMPT,
|
|
94
99
|
maxTokens: 300,
|
package/src/output-store.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// Output store — saves full raw output to disk when AI compresses it
|
|
2
|
-
// Agents can read the file for full detail.
|
|
2
|
+
// Agents can read the file for full detail. Tiered retention strategy.
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
7
|
|
|
8
8
|
const OUTPUTS_DIR = join(process.env.HOME ?? "~", ".terminal", "outputs");
|
|
9
|
-
const MAX_FILES = 50;
|
|
10
|
-
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
|
|
11
9
|
|
|
12
10
|
/** Ensure outputs directory exists */
|
|
13
11
|
function ensureDir() {
|
|
@@ -19,19 +17,59 @@ function hashOutput(command: string, output: string): string {
|
|
|
19
17
|
return createHash("md5").update(command + output.slice(0, 1000)).digest("hex").slice(0, 12);
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
/**
|
|
20
|
+
/** Tiered retention: recent = keep all, older = keep only high-value */
|
|
23
21
|
function rotate() {
|
|
24
22
|
try {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
25
|
+
const ONE_DAY = 24 * ONE_HOUR;
|
|
26
|
+
|
|
25
27
|
const files = readdirSync(OUTPUTS_DIR)
|
|
26
|
-
.
|
|
28
|
+
.filter(f => f.endsWith(".txt"))
|
|
29
|
+
.map(f => {
|
|
30
|
+
const path = join(OUTPUTS_DIR, f);
|
|
31
|
+
const stat = statSync(path);
|
|
32
|
+
return { name: f, path, mtime: stat.mtimeMs, size: stat.size };
|
|
33
|
+
})
|
|
27
34
|
.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const age = now - file.mtime;
|
|
38
|
+
|
|
39
|
+
// Last 1 hour: keep everything
|
|
40
|
+
if (age < ONE_HOUR) continue;
|
|
41
|
+
|
|
42
|
+
// Last 24 hours: keep outputs >2KB (meaningful compression)
|
|
43
|
+
if (age < ONE_DAY) {
|
|
44
|
+
if (file.size < 2000) {
|
|
45
|
+
try { unlinkSync(file.path); } catch {}
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Older than 24h: keep only >10KB (high-value saves)
|
|
51
|
+
if (file.size < 10000) {
|
|
52
|
+
try { unlinkSync(file.path); } catch {}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Older than 7 days: remove everything
|
|
57
|
+
if (age > 7 * ONE_DAY) {
|
|
58
|
+
try { unlinkSync(file.path); } catch {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Hard cap: never exceed 100 files or 10MB total
|
|
63
|
+
const remaining = readdirSync(OUTPUTS_DIR)
|
|
64
|
+
.filter(f => f.endsWith(".txt"))
|
|
65
|
+
.map(f => ({ path: join(OUTPUTS_DIR, f), mtime: statSync(join(OUTPUTS_DIR, f)).mtimeMs, size: statSync(join(OUTPUTS_DIR, f)).size }))
|
|
66
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
67
|
+
|
|
30
68
|
let totalSize = 0;
|
|
31
|
-
for (let i = 0; i <
|
|
32
|
-
totalSize +=
|
|
33
|
-
if (i >=
|
|
34
|
-
try { unlinkSync(
|
|
69
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
70
|
+
totalSize += remaining[i].size;
|
|
71
|
+
if (i >= 100 || totalSize > 10 * 1024 * 1024) {
|
|
72
|
+
try { unlinkSync(remaining[i].path); } catch {}
|
|
35
73
|
}
|
|
36
74
|
}
|
|
37
75
|
} catch {}
|
|
@@ -45,12 +83,10 @@ export function saveOutput(command: string, rawOutput: string): string {
|
|
|
45
83
|
const filename = `${hash}.txt`;
|
|
46
84
|
const filepath = join(OUTPUTS_DIR, filename);
|
|
47
85
|
|
|
48
|
-
// Write with command header
|
|
49
86
|
const content = `$ ${command}\n${"─".repeat(60)}\n${rawOutput}`;
|
|
50
87
|
writeFileSync(filepath, content, "utf8");
|
|
51
88
|
|
|
52
89
|
rotate();
|
|
53
|
-
|
|
54
90
|
return filepath;
|
|
55
91
|
}
|
|
56
92
|
|
|
@@ -63,3 +99,13 @@ export function formatOutputHint(filepath: string): string {
|
|
|
63
99
|
export function getOutputsDir(): string {
|
|
64
100
|
return OUTPUTS_DIR;
|
|
65
101
|
}
|
|
102
|
+
|
|
103
|
+
/** Manually purge all outputs */
|
|
104
|
+
export function purgeOutputs(): number {
|
|
105
|
+
if (!existsSync(OUTPUTS_DIR)) return 0;
|
|
106
|
+
let count = 0;
|
|
107
|
+
for (const f of readdirSync(OUTPUTS_DIR)) {
|
|
108
|
+
try { unlinkSync(join(OUTPUTS_DIR, f)); count++; } catch {}
|
|
109
|
+
}
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Tool profiles — config-driven AI enhancement for specific command categories
|
|
2
|
+
// Profiles are loaded from ~/.terminal/profiles/ (user-customizable)
|
|
3
|
+
// Each profile tells the AI how to handle a specific tool's output
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
export interface ToolProfile {
|
|
9
|
+
name: string;
|
|
10
|
+
/** Regex pattern to detect this tool in a command */
|
|
11
|
+
detect: string;
|
|
12
|
+
/** Hints injected into the AI output processor prompt */
|
|
13
|
+
hints: {
|
|
14
|
+
compress?: string; // How to compress this tool's output
|
|
15
|
+
errors?: string; // How to extract errors from this tool
|
|
16
|
+
success?: string; // What success looks like
|
|
17
|
+
};
|
|
18
|
+
/** Output handling */
|
|
19
|
+
output?: {
|
|
20
|
+
maxLines?: number; // Cap output before AI processing
|
|
21
|
+
preservePatterns?: string[]; // Regex patterns to always keep
|
|
22
|
+
stripPatterns?: string[]; // Regex patterns to always remove
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PROFILES_DIR = join(process.env.HOME ?? "~", ".terminal", "profiles");
|
|
27
|
+
|
|
28
|
+
/** Built-in profiles — sensible defaults, user can override */
|
|
29
|
+
const BUILTIN_PROFILES: ToolProfile[] = [
|
|
30
|
+
{
|
|
31
|
+
name: "git",
|
|
32
|
+
detect: "^git\\b",
|
|
33
|
+
hints: {
|
|
34
|
+
compress: "For git output: show branch, file counts, insertions/deletions summary. Collapse individual diffs to file-level stats.",
|
|
35
|
+
errors: "Git errors often include a suggested fix (e.g., 'did you mean X?'). Extract the suggestion.",
|
|
36
|
+
success: "Clean working tree, successful push/pull, merge complete.",
|
|
37
|
+
},
|
|
38
|
+
output: { preservePatterns: ["conflict", "CONFLICT", "fatal", "error", "diverged"] },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "test",
|
|
42
|
+
detect: "\\b(bun|npm|yarn|pnpm)\\s+(test|run\\s+test)|\\bpytest\\b|\\bcargo\\s+test\\b|\\bgo\\s+test\\b",
|
|
43
|
+
hints: {
|
|
44
|
+
compress: "For test output: show pass/fail counts FIRST, then list ONLY failing test names with error snippets. Skip passing tests entirely.",
|
|
45
|
+
errors: "Test failures have: test name, expected vs actual, stack trace. Extract all three.",
|
|
46
|
+
success: "All tests passing = one line: '✓ N tests pass, 0 fail'",
|
|
47
|
+
},
|
|
48
|
+
output: { preservePatterns: ["FAIL", "fail", "Error", "✗", "expected", "received"] },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "build",
|
|
52
|
+
detect: "\\b(tsc|bun\\s+run\\s+build|npm\\s+run\\s+build|cargo\\s+build|go\\s+build|make)\\b",
|
|
53
|
+
hints: {
|
|
54
|
+
compress: "For build output: if success with no errors, say '✓ Build succeeded'. If errors, list each error with file:line and message.",
|
|
55
|
+
errors: "Build errors have file:line:column format. Group by file.",
|
|
56
|
+
success: "Empty output or exit 0 = build succeeded.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "lint",
|
|
61
|
+
detect: "\\b(eslint|biome|ruff|clippy|golangci-lint|prettier|tsc\\s+--noEmit)\\b",
|
|
62
|
+
hints: {
|
|
63
|
+
compress: "For lint output: group violations by rule name, show count per rule, one example per rule. Skip clean files.",
|
|
64
|
+
errors: "Lint violations: file:line rule-name message. Group by rule.",
|
|
65
|
+
},
|
|
66
|
+
output: { maxLines: 100 },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "install",
|
|
70
|
+
detect: "\\b(npm\\s+install|bun\\s+install|yarn|pip\\s+install|cargo\\s+build|go\\s+mod)\\b",
|
|
71
|
+
hints: {
|
|
72
|
+
compress: "For install output: show only errors and final summary (packages added/removed/updated). Strip progress bars, funding notices, deprecation warnings.",
|
|
73
|
+
},
|
|
74
|
+
output: { stripPatterns: ["npm warn", "packages are looking for funding", "run `npm fund`"] },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "find",
|
|
78
|
+
detect: "^find\\b",
|
|
79
|
+
hints: {
|
|
80
|
+
compress: "For find output: if >50 results, group by top-level directory with counts. Show first 10 results as examples.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "docker",
|
|
85
|
+
detect: "\\b(docker|kubectl|helm)\\b",
|
|
86
|
+
hints: {
|
|
87
|
+
compress: "For container output: show container status, image, ports. Strip pull progress and layer hashes.",
|
|
88
|
+
errors: "Docker errors: extract the error message after 'Error response from daemon:'",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/** Load user profiles from ~/.terminal/profiles/ */
|
|
94
|
+
function loadUserProfiles(): ToolProfile[] {
|
|
95
|
+
if (!existsSync(PROFILES_DIR)) return [];
|
|
96
|
+
|
|
97
|
+
const profiles: ToolProfile[] = [];
|
|
98
|
+
try {
|
|
99
|
+
for (const file of readdirSync(PROFILES_DIR)) {
|
|
100
|
+
if (!file.endsWith(".json")) continue;
|
|
101
|
+
try {
|
|
102
|
+
const content = JSON.parse(readFileSync(join(PROFILES_DIR, file), "utf8"));
|
|
103
|
+
if (content.name && content.detect) profiles.push(content as ToolProfile);
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
return profiles;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get all profiles — user profiles override builtins by name */
|
|
111
|
+
export function getProfiles(): ToolProfile[] {
|
|
112
|
+
const user = loadUserProfiles();
|
|
113
|
+
const userNames = new Set(user.map(p => p.name));
|
|
114
|
+
const builtins = BUILTIN_PROFILES.filter(p => !userNames.has(p.name));
|
|
115
|
+
return [...user, ...builtins];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Find the matching profile for a command */
|
|
119
|
+
export function matchProfile(command: string): ToolProfile | null {
|
|
120
|
+
for (const profile of getProfiles()) {
|
|
121
|
+
try {
|
|
122
|
+
if (new RegExp(profile.detect).test(command)) return profile;
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Format profile hints for injection into AI prompt */
|
|
129
|
+
export function formatProfileHints(command: string): string {
|
|
130
|
+
const profile = matchProfile(command);
|
|
131
|
+
if (!profile) return "";
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [`TOOL PROFILE (${profile.name}):`];
|
|
134
|
+
if (profile.hints.compress) lines.push(` Compression: ${profile.hints.compress}`);
|
|
135
|
+
if (profile.hints.errors) lines.push(` Errors: ${profile.hints.errors}`);
|
|
136
|
+
if (profile.hints.success) lines.push(` Success: ${profile.hints.success}`);
|
|
137
|
+
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|