@hasna/terminal 2.0.2 → 2.0.3

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/ai.js CHANGED
@@ -107,23 +107,66 @@ function detectProjectContext() {
107
107
  catch { }
108
108
  }
109
109
  // Python
110
- if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) {
111
- parts.push("Project: Python. Use pip/python commands.");
110
+ if (existsSync(join(cwd, "pyproject.toml"))) {
111
+ try {
112
+ const pyproject = readFileSync(join(cwd, "pyproject.toml"), "utf8");
113
+ const nameMatch = pyproject.match(/name\s*=\s*"([^"]+)"/);
114
+ const versionMatch = pyproject.match(/version\s*=\s*"([^"]+)"/);
115
+ parts.push(`Project: ${nameMatch?.[1] ?? "Python"}${versionMatch ? `@${versionMatch[1]}` : ""} (Python)`);
116
+ }
117
+ catch {
118
+ parts.push("Project: Python (pyproject.toml found)");
119
+ }
120
+ parts.push("Use pip/python/pytest commands. Test: pytest. Build: python -m build.");
121
+ }
122
+ else if (existsSync(join(cwd, "requirements.txt"))) {
123
+ parts.push("Project: Python (requirements.txt). Use pip/python/pytest commands.");
112
124
  }
113
125
  // Go
114
126
  if (existsSync(join(cwd, "go.mod"))) {
115
- parts.push("Project: Go. Use go build/test/run commands.");
127
+ try {
128
+ const gomod = readFileSync(join(cwd, "go.mod"), "utf8");
129
+ const moduleMatch = gomod.match(/module\s+(\S+)/);
130
+ parts.push(`Project: ${moduleMatch?.[1] ?? "Go"} (Go module)`);
131
+ }
132
+ catch {
133
+ parts.push("Project: Go (go.mod found)");
134
+ }
135
+ parts.push("Use go build/test/run. Test: go test ./... Build: go build.");
116
136
  }
117
137
  // Rust
118
138
  if (existsSync(join(cwd, "Cargo.toml"))) {
119
- parts.push("Project: Rust. Use cargo build/test/run commands.");
139
+ try {
140
+ const cargo = readFileSync(join(cwd, "Cargo.toml"), "utf8");
141
+ const nameMatch = cargo.match(/name\s*=\s*"([^"]+)"/);
142
+ const versionMatch = cargo.match(/version\s*=\s*"([^"]+)"/);
143
+ parts.push(`Project: ${nameMatch?.[1] ?? "Rust"}${versionMatch ? `@${versionMatch[1]}` : ""} (Rust/Cargo)`);
144
+ }
145
+ catch {
146
+ parts.push("Project: Rust (Cargo.toml found)");
147
+ }
148
+ parts.push("Use cargo build/test/run. Test: cargo test. Build: cargo build --release.");
120
149
  }
121
150
  // Java
122
151
  if (existsSync(join(cwd, "pom.xml"))) {
123
- parts.push("Project: Java/Maven. Use mvn commands.");
152
+ parts.push("Project: Java/Maven. Use mvn commands. Test: mvn test. Build: mvn package.");
124
153
  }
125
154
  if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
126
- parts.push("Project: Java/Gradle. Use gradle commands.");
155
+ parts.push("Project: Java/Gradle. Use gradle commands. Test: gradle test. Build: gradle build.");
156
+ }
157
+ // Docker
158
+ if (existsSync(join(cwd, "Dockerfile")) || existsSync(join(cwd, "docker-compose.yml")) || existsSync(join(cwd, "docker-compose.yaml"))) {
159
+ parts.push("Docker: Dockerfile/docker-compose present. Container commands available.");
160
+ }
161
+ // Makefile
162
+ if (existsSync(join(cwd, "Makefile"))) {
163
+ try {
164
+ const { execSync: execS } = require("child_process");
165
+ const targets = execS("grep -E '^[a-zA-Z_-]+:' Makefile | head -10 | cut -d: -f1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
166
+ if (targets)
167
+ parts.push(`Makefile targets: ${targets.split("\n").join(", ")}`);
168
+ }
169
+ catch { }
127
170
  }
128
171
  // Directory structure — so AI knows actual paths (not guessed ones)
129
172
  try {
@@ -15,7 +15,9 @@ RULES:
15
15
  - Use symbols: ✓ for success/yes, ✗ for failure/no, ⚠ for warnings
16
16
  - Maximum 8 lines
17
17
  - Keep errors/failures verbatim
18
- - Be direct and concise — the user wants an ANSWER, not a data dump`;
18
+ - Be direct and concise — the user wants an ANSWER, not a data dump
19
+ - For TEST OUTPUT: look for "X pass" and "X fail" lines. These are DEFINITIVE. If you see "42 pass, 0 fail" in the output, the answer is "42 tests pass, 0 fail." NEVER say "no tests found" or "incomplete" when pass/fail counts are visible.
20
+ - For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.`;
19
21
  /**
20
22
  * Process command output through AI summarization.
21
23
  * Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
@@ -45,8 +47,15 @@ export async function processOutput(command, output, originalPrompt) {
45
47
  output.slice(-tailChars);
46
48
  }
47
49
  try {
50
+ // Pre-parse: extract test counts so AI can't misread them
51
+ let preParseHint = "";
52
+ const passMatch = output.match(/(\d+)\s+pass/i);
53
+ const failMatch = output.match(/(\d+)\s+fail/i);
54
+ if (passMatch || failMatch) {
55
+ preParseHint = `\nPRE-PARSED TEST RESULTS: ${passMatch?.[1] ?? 0} passed, ${failMatch?.[1] ?? 0} failed. USE THESE NUMBERS.`;
56
+ }
48
57
  const provider = getProvider();
49
- const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`, {
58
+ const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}${preParseHint}\nOutput (${lines.length} lines):\n${toSummarize}`, {
50
59
  system: SUMMARIZE_PROMPT,
51
60
  maxTokens: 300,
52
61
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ai.ts CHANGED
@@ -130,26 +130,59 @@ function detectProjectContext(): string {
130
130
  }
131
131
 
132
132
  // Python
133
- if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) {
134
- parts.push("Project: Python. Use pip/python commands.");
133
+ if (existsSync(join(cwd, "pyproject.toml"))) {
134
+ try {
135
+ const pyproject = readFileSync(join(cwd, "pyproject.toml"), "utf8");
136
+ const nameMatch = pyproject.match(/name\s*=\s*"([^"]+)"/);
137
+ const versionMatch = pyproject.match(/version\s*=\s*"([^"]+)"/);
138
+ parts.push(`Project: ${nameMatch?.[1] ?? "Python"}${versionMatch ? `@${versionMatch[1]}` : ""} (Python)`);
139
+ } catch { parts.push("Project: Python (pyproject.toml found)"); }
140
+ parts.push("Use pip/python/pytest commands. Test: pytest. Build: python -m build.");
141
+ } else if (existsSync(join(cwd, "requirements.txt"))) {
142
+ parts.push("Project: Python (requirements.txt). Use pip/python/pytest commands.");
135
143
  }
136
144
 
137
145
  // Go
138
146
  if (existsSync(join(cwd, "go.mod"))) {
139
- parts.push("Project: Go. Use go build/test/run commands.");
147
+ try {
148
+ const gomod = readFileSync(join(cwd, "go.mod"), "utf8");
149
+ const moduleMatch = gomod.match(/module\s+(\S+)/);
150
+ parts.push(`Project: ${moduleMatch?.[1] ?? "Go"} (Go module)`);
151
+ } catch { parts.push("Project: Go (go.mod found)"); }
152
+ parts.push("Use go build/test/run. Test: go test ./... Build: go build.");
140
153
  }
141
154
 
142
155
  // Rust
143
156
  if (existsSync(join(cwd, "Cargo.toml"))) {
144
- parts.push("Project: Rust. Use cargo build/test/run commands.");
157
+ try {
158
+ const cargo = readFileSync(join(cwd, "Cargo.toml"), "utf8");
159
+ const nameMatch = cargo.match(/name\s*=\s*"([^"]+)"/);
160
+ const versionMatch = cargo.match(/version\s*=\s*"([^"]+)"/);
161
+ parts.push(`Project: ${nameMatch?.[1] ?? "Rust"}${versionMatch ? `@${versionMatch[1]}` : ""} (Rust/Cargo)`);
162
+ } catch { parts.push("Project: Rust (Cargo.toml found)"); }
163
+ parts.push("Use cargo build/test/run. Test: cargo test. Build: cargo build --release.");
145
164
  }
146
165
 
147
166
  // Java
148
167
  if (existsSync(join(cwd, "pom.xml"))) {
149
- parts.push("Project: Java/Maven. Use mvn commands.");
168
+ parts.push("Project: Java/Maven. Use mvn commands. Test: mvn test. Build: mvn package.");
150
169
  }
151
170
  if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
152
- parts.push("Project: Java/Gradle. Use gradle commands.");
171
+ parts.push("Project: Java/Gradle. Use gradle commands. Test: gradle test. Build: gradle build.");
172
+ }
173
+
174
+ // Docker
175
+ if (existsSync(join(cwd, "Dockerfile")) || existsSync(join(cwd, "docker-compose.yml")) || existsSync(join(cwd, "docker-compose.yaml"))) {
176
+ parts.push("Docker: Dockerfile/docker-compose present. Container commands available.");
177
+ }
178
+
179
+ // Makefile
180
+ if (existsSync(join(cwd, "Makefile"))) {
181
+ try {
182
+ const { execSync: execS } = require("child_process");
183
+ const targets = execS("grep -E '^[a-zA-Z_-]+:' Makefile | head -10 | cut -d: -f1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
184
+ if (targets) parts.push(`Makefile targets: ${targets.split("\n").join(", ")}`);
185
+ } catch {}
153
186
  }
154
187
 
155
188
  // Directory structure — so AI knows actual paths (not guessed ones)
@@ -39,7 +39,9 @@ RULES:
39
39
  - Use symbols: ✓ for success/yes, ✗ for failure/no, ⚠ for warnings
40
40
  - Maximum 8 lines
41
41
  - Keep errors/failures verbatim
42
- - Be direct and concise — the user wants an ANSWER, not a data dump`;
42
+ - Be direct and concise — the user wants an ANSWER, not a data dump
43
+ - For TEST OUTPUT: look for "X pass" and "X fail" lines. These are DEFINITIVE. If you see "42 pass, 0 fail" in the output, the answer is "42 tests pass, 0 fail." NEVER say "no tests found" or "incomplete" when pass/fail counts are visible.
44
+ - For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.`;
43
45
 
44
46
  /**
45
47
  * Process command output through AI summarization.
@@ -77,9 +79,17 @@ export async function processOutput(
77
79
  }
78
80
 
79
81
  try {
82
+ // Pre-parse: extract test counts so AI can't misread them
83
+ let preParseHint = "";
84
+ const passMatch = output.match(/(\d+)\s+pass/i);
85
+ const failMatch = output.match(/(\d+)\s+fail/i);
86
+ if (passMatch || failMatch) {
87
+ preParseHint = `\nPRE-PARSED TEST RESULTS: ${passMatch?.[1] ?? 0} passed, ${failMatch?.[1] ?? 0} failed. USE THESE NUMBERS.`;
88
+ }
89
+
80
90
  const provider = getProvider();
81
91
  const summary = await provider.complete(
82
- `${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`,
92
+ `${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}${preParseHint}\nOutput (${lines.length} lines):\n${toSummarize}`,
83
93
  {
84
94
  system: SUMMARIZE_PROMPT,
85
95
  maxTokens: 300,