@hasna/terminal 2.0.1 → 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
@@ -94,33 +94,79 @@ function detectProjectContext() {
94
94
  if (existsSync(pkgPath)) {
95
95
  try {
96
96
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
97
- parts.push(`Project: Node.js/TypeScript (package.json found)`);
97
+ parts.push(`Project: ${pkg.name}@${pkg.version} (Node.js/TypeScript)`);
98
+ parts.push(`npm package: ${pkg.name} (use this name for npm view, npm info, etc.)`);
98
99
  if (pkg.scripts) {
99
100
  const scripts = Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).slice(0, 8);
100
101
  parts.push(`Available scripts: ${scripts.join(", ")}`);
101
102
  }
103
+ if (pkg.dependencies)
104
+ parts.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
102
105
  parts.push(`Use npm/bun/pnpm commands, NOT maven/gradle/cargo.`);
103
106
  }
104
107
  catch { }
105
108
  }
106
109
  // Python
107
- if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) {
108
- 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.");
109
124
  }
110
125
  // Go
111
126
  if (existsSync(join(cwd, "go.mod"))) {
112
- 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.");
113
136
  }
114
137
  // Rust
115
138
  if (existsSync(join(cwd, "Cargo.toml"))) {
116
- 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.");
117
149
  }
118
150
  // Java
119
151
  if (existsSync(join(cwd, "pom.xml"))) {
120
- parts.push("Project: Java/Maven. Use mvn commands.");
152
+ parts.push("Project: Java/Maven. Use mvn commands. Test: mvn test. Build: mvn package.");
121
153
  }
122
154
  if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
123
- 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 { }
124
170
  }
125
171
  // Directory structure — so AI knows actual paths (not guessed ones)
126
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.1",
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
@@ -118,36 +118,71 @@ function detectProjectContext(): string {
118
118
  if (existsSync(pkgPath)) {
119
119
  try {
120
120
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
121
- parts.push(`Project: Node.js/TypeScript (package.json found)`);
121
+ parts.push(`Project: ${pkg.name}@${pkg.version} (Node.js/TypeScript)`);
122
+ parts.push(`npm package: ${pkg.name} (use this name for npm view, npm info, etc.)`);
122
123
  if (pkg.scripts) {
123
124
  const scripts = Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).slice(0, 8);
124
125
  parts.push(`Available scripts: ${scripts.join(", ")}`);
125
126
  }
127
+ if (pkg.dependencies) parts.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
126
128
  parts.push(`Use npm/bun/pnpm commands, NOT maven/gradle/cargo.`);
127
129
  } catch {}
128
130
  }
129
131
 
130
132
  // Python
131
- if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) {
132
- 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.");
133
143
  }
134
144
 
135
145
  // Go
136
146
  if (existsSync(join(cwd, "go.mod"))) {
137
- 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.");
138
153
  }
139
154
 
140
155
  // Rust
141
156
  if (existsSync(join(cwd, "Cargo.toml"))) {
142
- 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.");
143
164
  }
144
165
 
145
166
  // Java
146
167
  if (existsSync(join(cwd, "pom.xml"))) {
147
- parts.push("Project: Java/Maven. Use mvn commands.");
168
+ parts.push("Project: Java/Maven. Use mvn commands. Test: mvn test. Build: mvn package.");
148
169
  }
149
170
  if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
150
- 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 {}
151
186
  }
152
187
 
153
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,