@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 +53 -7
- package/dist/output-processor.js +11 -2
- package/package.json +1 -1
- package/src/ai.ts +42 -7
- package/src/output-processor.ts +12 -2
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
|
|
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, "
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/output-processor.js
CHANGED
|
@@ -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
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
|
|
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, "
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/src/output-processor.ts
CHANGED
|
@@ -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,
|