@hasna/terminal 2.0.4 → 2.2.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 +24 -10
- package/package.json +1 -1
- package/src/ai.ts +46 -113
- package/src/cli.tsx +23 -10
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +202 -0
- package/src/output-processor.ts +7 -18
- 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/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/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,
|
package/src/providers/base.ts
CHANGED
|
@@ -24,9 +24,11 @@ export interface LLMProvider {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface ProviderConfig {
|
|
27
|
-
provider: "cerebras" | "anthropic" | "auto";
|
|
27
|
+
provider: "cerebras" | "anthropic" | "groq" | "xai" | "auto";
|
|
28
28
|
cerebrasModel?: string;
|
|
29
29
|
anthropicModel?: string;
|
|
30
|
+
groqModel?: string;
|
|
31
|
+
xaiModel?: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export const DEFAULT_PROVIDER_CONFIG: ProviderConfig = {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Groq provider — uses OpenAI-compatible API
|
|
2
|
+
// Ultra-fast inference. Supports Llama, Qwen, Kimi models.
|
|
3
|
+
|
|
4
|
+
import type { LLMProvider, ProviderOptions, StreamCallbacks } from "./base.js";
|
|
5
|
+
|
|
6
|
+
const GROQ_BASE_URL = "https://api.groq.com/openai/v1";
|
|
7
|
+
const DEFAULT_MODEL = "openai/gpt-oss-120b";
|
|
8
|
+
|
|
9
|
+
export class GroqProvider implements LLMProvider {
|
|
10
|
+
readonly name = "groq";
|
|
11
|
+
private apiKey: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.apiKey = process.env.GROQ_API_KEY ?? "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
isAvailable(): boolean {
|
|
18
|
+
return !!process.env.GROQ_API_KEY;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async complete(prompt: string, options: ProviderOptions): Promise<string> {
|
|
22
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
23
|
+
const res = await fetch(`${GROQ_BASE_URL}/chat/completions`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
model,
|
|
31
|
+
max_tokens: options.maxTokens ?? 256,
|
|
32
|
+
messages: [
|
|
33
|
+
{ role: "system", content: options.system },
|
|
34
|
+
{ role: "user", content: prompt },
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
throw new Error(`Groq API error ${res.status}: ${text}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const json = (await res.json()) as any;
|
|
45
|
+
return (json.choices?.[0]?.message?.content ?? "").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async stream(prompt: string, options: ProviderOptions, callbacks: StreamCallbacks): Promise<string> {
|
|
49
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
50
|
+
const res = await fetch(`${GROQ_BASE_URL}/chat/completions`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model,
|
|
58
|
+
max_tokens: options.maxTokens ?? 256,
|
|
59
|
+
stream: true,
|
|
60
|
+
messages: [
|
|
61
|
+
{ role: "system", content: options.system },
|
|
62
|
+
{ role: "user", content: prompt },
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
throw new Error(`Groq API error ${res.status}: ${text}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let result = "";
|
|
73
|
+
const reader = res.body?.getReader();
|
|
74
|
+
if (!reader) throw new Error("No response body");
|
|
75
|
+
|
|
76
|
+
const decoder = new TextDecoder();
|
|
77
|
+
let buffer = "";
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done) break;
|
|
82
|
+
|
|
83
|
+
buffer += decoder.decode(value, { stream: true });
|
|
84
|
+
const lines = buffer.split("\n");
|
|
85
|
+
buffer = lines.pop() ?? "";
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
90
|
+
const data = trimmed.slice(6);
|
|
91
|
+
if (data === "[DONE]") break;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(data) as any;
|
|
95
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
96
|
+
if (delta) {
|
|
97
|
+
result += delta;
|
|
98
|
+
callbacks.onToken(result.trim());
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// skip malformed chunks
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result.trim();
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/providers/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { LLMProvider, ProviderConfig } from "./base.js";
|
|
|
4
4
|
import { DEFAULT_PROVIDER_CONFIG } from "./base.js";
|
|
5
5
|
import { AnthropicProvider } from "./anthropic.js";
|
|
6
6
|
import { CerebrasProvider } from "./cerebras.js";
|
|
7
|
+
import { GroqProvider } from "./groq.js";
|
|
8
|
+
import { XaiProvider } from "./xai.js";
|
|
7
9
|
|
|
8
10
|
export type { LLMProvider, ProviderOptions, StreamCallbacks, ProviderConfig } from "./base.js";
|
|
9
11
|
export { DEFAULT_PROVIDER_CONFIG } from "./base.js";
|
|
@@ -37,17 +39,37 @@ function resolveProvider(config: ProviderConfig): LLMProvider {
|
|
|
37
39
|
return p;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
if (config.provider === "groq") {
|
|
43
|
+
const p = new GroqProvider();
|
|
44
|
+
if (!p.isAvailable()) throw new Error("GROQ_API_KEY not set. Run: export GROQ_API_KEY=your-key");
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (config.provider === "xai") {
|
|
49
|
+
const p = new XaiProvider();
|
|
50
|
+
if (!p.isAvailable()) throw new Error("XAI_API_KEY not set. Run: export XAI_API_KEY=your-key");
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// auto: prefer xAI (code-optimized), then Cerebras, then Groq, then Anthropic
|
|
55
|
+
const xai = new XaiProvider();
|
|
56
|
+
if (xai.isAvailable()) return xai;
|
|
57
|
+
|
|
41
58
|
const cerebras = new CerebrasProvider();
|
|
42
59
|
if (cerebras.isAvailable()) return cerebras;
|
|
43
60
|
|
|
61
|
+
const groq = new GroqProvider();
|
|
62
|
+
if (groq.isAvailable()) return groq;
|
|
63
|
+
|
|
44
64
|
const anthropic = new AnthropicProvider();
|
|
45
65
|
if (anthropic.isAvailable()) return anthropic;
|
|
46
66
|
|
|
47
67
|
throw new Error(
|
|
48
68
|
"No API key found. Set one of:\n" +
|
|
49
69
|
" export CEREBRAS_API_KEY=your-key (free, open-source)\n" +
|
|
50
|
-
" export
|
|
70
|
+
" export GROQ_API_KEY=your-key (free, fast)\n" +
|
|
71
|
+
" export XAI_API_KEY=your-key (Grok, code-optimized)\n" +
|
|
72
|
+
" export ANTHROPIC_API_KEY=your-key (Claude)"
|
|
51
73
|
);
|
|
52
74
|
}
|
|
53
75
|
|
|
@@ -55,6 +77,8 @@ function resolveProvider(config: ProviderConfig): LLMProvider {
|
|
|
55
77
|
export function availableProviders(): { name: string; available: boolean }[] {
|
|
56
78
|
return [
|
|
57
79
|
{ name: "cerebras", available: new CerebrasProvider().isAvailable() },
|
|
80
|
+
{ name: "groq", available: new GroqProvider().isAvailable() },
|
|
81
|
+
{ name: "xai", available: new XaiProvider().isAvailable() },
|
|
58
82
|
{ name: "anthropic", available: new AnthropicProvider().isAvailable() },
|
|
59
83
|
];
|
|
60
84
|
}
|
|
@@ -4,9 +4,11 @@ import { availableProviders, resetProvider } from "./index.js";
|
|
|
4
4
|
describe("providers", () => {
|
|
5
5
|
it("lists available providers", () => {
|
|
6
6
|
const providers = availableProviders();
|
|
7
|
-
expect(providers.length).toBe(
|
|
7
|
+
expect(providers.length).toBe(4);
|
|
8
8
|
expect(providers[0].name).toBe("cerebras");
|
|
9
|
-
expect(providers[1].name).toBe("
|
|
9
|
+
expect(providers[1].name).toBe("groq");
|
|
10
|
+
expect(providers[2].name).toBe("xai");
|
|
11
|
+
expect(providers[3].name).toBe("anthropic");
|
|
10
12
|
});
|
|
11
13
|
|
|
12
14
|
it("resetProvider clears cache", () => {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// xAI/Grok provider — uses OpenAI-compatible API
|
|
2
|
+
// grok-code-fast-1 for code tasks, grok-4-fast for general queries.
|
|
3
|
+
|
|
4
|
+
import type { LLMProvider, ProviderOptions, StreamCallbacks } from "./base.js";
|
|
5
|
+
|
|
6
|
+
const XAI_BASE_URL = "https://api.x.ai/v1";
|
|
7
|
+
const DEFAULT_MODEL = "grok-code-fast-1";
|
|
8
|
+
|
|
9
|
+
export class XaiProvider implements LLMProvider {
|
|
10
|
+
readonly name = "xai";
|
|
11
|
+
private apiKey: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.apiKey = process.env.XAI_API_KEY ?? "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
isAvailable(): boolean {
|
|
18
|
+
return !!process.env.XAI_API_KEY;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async complete(prompt: string, options: ProviderOptions): Promise<string> {
|
|
22
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
23
|
+
const res = await fetch(`${XAI_BASE_URL}/chat/completions`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
model,
|
|
31
|
+
max_tokens: options.maxTokens ?? 256,
|
|
32
|
+
messages: [
|
|
33
|
+
{ role: "system", content: options.system },
|
|
34
|
+
{ role: "user", content: prompt },
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
throw new Error(`xAI API error ${res.status}: ${text}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const json = (await res.json()) as any;
|
|
45
|
+
return (json.choices?.[0]?.message?.content ?? "").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async stream(prompt: string, options: ProviderOptions, callbacks: StreamCallbacks): Promise<string> {
|
|
49
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
50
|
+
const res = await fetch(`${XAI_BASE_URL}/chat/completions`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model,
|
|
58
|
+
max_tokens: options.maxTokens ?? 256,
|
|
59
|
+
stream: true,
|
|
60
|
+
messages: [
|
|
61
|
+
{ role: "system", content: options.system },
|
|
62
|
+
{ role: "user", content: prompt },
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
throw new Error(`xAI API error ${res.status}: ${text}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let result = "";
|
|
73
|
+
const reader = res.body?.getReader();
|
|
74
|
+
if (!reader) throw new Error("No response body");
|
|
75
|
+
|
|
76
|
+
const decoder = new TextDecoder();
|
|
77
|
+
let buffer = "";
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done) break;
|
|
82
|
+
|
|
83
|
+
buffer += decoder.decode(value, { stream: true });
|
|
84
|
+
const lines = buffer.split("\n");
|
|
85
|
+
buffer = lines.pop() ?? "";
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
90
|
+
const data = trimmed.slice(6);
|
|
91
|
+
if (data === "[DONE]") break;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(data) as any;
|
|
95
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
96
|
+
if (delta) {
|
|
97
|
+
result += delta;
|
|
98
|
+
callbacks.onToken(result.trim());
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// skip malformed chunks
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result.trim();
|
|
107
|
+
}
|
|
108
|
+
}
|