@hasna/terminal 0.2.0 → 0.2.2
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/App.js +8 -5
- package/dist/ai.js +34 -19
- package/dist/providers/cerebras.js +1 -1
- package/package.json +1 -1
- package/src/App.tsx +10 -7
- package/src/ai.ts +39 -17
- package/src/providers/cerebras.ts +1 -1
package/dist/App.js
CHANGED
|
@@ -40,7 +40,7 @@ function maybeCd(command) {
|
|
|
40
40
|
function newTab(id, cwd) {
|
|
41
41
|
return {
|
|
42
42
|
id, cwd,
|
|
43
|
-
scroll: [],
|
|
43
|
+
scroll: [], sessionEntries: [], sessionNl: [],
|
|
44
44
|
phase: { type: "input", value: "", cursor: 0, histIdx: -1, raw: false },
|
|
45
45
|
streamLines: [],
|
|
46
46
|
};
|
|
@@ -83,10 +83,13 @@ export default function App() {
|
|
|
83
83
|
const commitStream = (nl, cmd, lines, error) => {
|
|
84
84
|
const truncated = lines.length > MAX_LINES;
|
|
85
85
|
const filePaths = !error ? extractFilePaths(lines) : [];
|
|
86
|
+
// Build short output summary for session context (first 10 lines)
|
|
87
|
+
const shortOutput = lines.slice(0, 10).join("\n") + (lines.length > 10 ? `\n... (${lines.length} lines total)` : "");
|
|
88
|
+
const entry = { nl, cmd, output: shortOutput, error: error || undefined };
|
|
86
89
|
updateTab(t => ({
|
|
87
90
|
...t,
|
|
88
91
|
streamLines: [],
|
|
89
|
-
|
|
92
|
+
sessionEntries: [...t.sessionEntries.slice(-9), entry],
|
|
90
93
|
scroll: [...t.scroll, {
|
|
91
94
|
nl, cmd,
|
|
92
95
|
lines: truncated ? lines.slice(0, MAX_LINES) : lines,
|
|
@@ -137,10 +140,10 @@ export default function App() {
|
|
|
137
140
|
await runPhase(nl, nl, true);
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
140
|
-
const
|
|
143
|
+
const sessionEntries = tabs[activeTab].sessionEntries;
|
|
141
144
|
setPhase({ type: "thinking", nl, partial: "" });
|
|
142
145
|
try {
|
|
143
|
-
const command = await translateToCommand(nl, config.permissions,
|
|
146
|
+
const command = await translateToCommand(nl, config.permissions, sessionEntries, partial => setPhase({ type: "thinking", nl, partial }));
|
|
144
147
|
const blocked = checkPermissions(command, config.permissions);
|
|
145
148
|
if (blocked) {
|
|
146
149
|
pushScroll({ nl, cmd: command, lines: [`blocked: ${blocked}`], truncated: false, error: true });
|
|
@@ -309,7 +312,7 @@ export default function App() {
|
|
|
309
312
|
const { nl, command, errorOutput } = phase;
|
|
310
313
|
setPhase({ type: "thinking", nl, partial: "" });
|
|
311
314
|
try {
|
|
312
|
-
const fixed = await fixCommand(nl, command, errorOutput, config.permissions, tab.
|
|
315
|
+
const fixed = await fixCommand(nl, command, errorOutput, config.permissions, tab.sessionEntries);
|
|
313
316
|
const danger = isIrreversible(fixed);
|
|
314
317
|
if (!config.confirm && !danger) {
|
|
315
318
|
await runPhase(nl, fixed, false);
|
package/dist/ai.js
CHANGED
|
@@ -20,10 +20,10 @@ function pickModel(nl) {
|
|
|
20
20
|
pick: isComplex ? "smart" : "fast",
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
// Cerebras —
|
|
23
|
+
// Cerebras — fast model for simple, smart model for complex
|
|
24
24
|
return {
|
|
25
|
-
fast: "
|
|
26
|
-
smart: "
|
|
25
|
+
fast: "llama3.1-8b",
|
|
26
|
+
smart: "llama3.1-8b",
|
|
27
27
|
pick: isComplex ? "smart" : "fast",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -56,7 +56,7 @@ export function checkPermissions(command, perms) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
59
|
-
function buildSystemPrompt(perms,
|
|
59
|
+
function buildSystemPrompt(perms, sessionEntries) {
|
|
60
60
|
const restrictions = [];
|
|
61
61
|
if (!perms.destructive)
|
|
62
62
|
restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
|
|
@@ -71,25 +71,40 @@ function buildSystemPrompt(perms, sessionCmds) {
|
|
|
71
71
|
const restrictionBlock = restrictions.length > 0
|
|
72
72
|
? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
|
|
73
73
|
: "";
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
let contextBlock = "";
|
|
75
|
+
if (sessionEntries.length > 0) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const e of sessionEntries.slice(-5)) { // last 5 interactions
|
|
78
|
+
lines.push(`> ${e.nl}`);
|
|
79
|
+
lines.push(`$ ${e.cmd}`);
|
|
80
|
+
if (e.output)
|
|
81
|
+
lines.push(e.output);
|
|
82
|
+
if (e.error)
|
|
83
|
+
lines.push("(command failed)");
|
|
84
|
+
}
|
|
85
|
+
contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
|
|
86
|
+
}
|
|
77
87
|
return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
|
|
88
|
+
The user describes what they want in plain English. You translate to the exact shell command.
|
|
89
|
+
Pay attention to session history — when the user says "inside X folder" they mean a folder visible in the previous output.
|
|
90
|
+
If the user refers to a relative path from a previous command, resolve it correctly.
|
|
78
91
|
cwd: ${process.cwd()}
|
|
79
92
|
shell: zsh / macOS${restrictionBlock}${contextBlock}`;
|
|
80
93
|
}
|
|
81
94
|
// ── streaming translate ───────────────────────────────────────────────────────
|
|
82
|
-
export async function translateToCommand(nl, perms,
|
|
83
|
-
// cache
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
export async function translateToCommand(nl, perms, sessionEntries, onToken) {
|
|
96
|
+
// Only use cache when there's no session context (context makes same NL produce different commands)
|
|
97
|
+
if (sessionEntries.length === 0) {
|
|
98
|
+
const cached = cacheGet(nl);
|
|
99
|
+
if (cached) {
|
|
100
|
+
onToken?.(cached);
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
88
103
|
}
|
|
89
104
|
const provider = getProvider();
|
|
90
105
|
const routing = pickModel(nl);
|
|
91
106
|
const model = routing.pick === "smart" ? routing.smart : routing.fast;
|
|
92
|
-
const system = buildSystemPrompt(perms,
|
|
107
|
+
const system = buildSystemPrompt(perms, sessionEntries);
|
|
93
108
|
let text;
|
|
94
109
|
if (onToken) {
|
|
95
110
|
text = await provider.stream(nl, { model, maxTokens: 256, system }, {
|
|
@@ -105,10 +120,10 @@ export async function translateToCommand(nl, perms, sessionCmds, onToken) {
|
|
|
105
120
|
return text;
|
|
106
121
|
}
|
|
107
122
|
// ── prefetch ──────────────────────────────────────────────────────────────────
|
|
108
|
-
export function prefetchNext(lastNl, perms,
|
|
109
|
-
if (cacheGet(lastNl))
|
|
123
|
+
export function prefetchNext(lastNl, perms, sessionEntries) {
|
|
124
|
+
if (sessionEntries.length === 0 && cacheGet(lastNl))
|
|
110
125
|
return;
|
|
111
|
-
translateToCommand(lastNl, perms,
|
|
126
|
+
translateToCommand(lastNl, perms, sessionEntries).catch(() => { });
|
|
112
127
|
}
|
|
113
128
|
// ── explain ───────────────────────────────────────────────────────────────────
|
|
114
129
|
export async function explainCommand(command) {
|
|
@@ -121,13 +136,13 @@ export async function explainCommand(command) {
|
|
|
121
136
|
});
|
|
122
137
|
}
|
|
123
138
|
// ── auto-fix ──────────────────────────────────────────────────────────────────
|
|
124
|
-
export async function fixCommand(originalNl, failedCommand, errorOutput, perms,
|
|
139
|
+
export async function fixCommand(originalNl, failedCommand, errorOutput, perms, sessionEntries) {
|
|
125
140
|
const provider = getProvider();
|
|
126
141
|
const routing = pickModel(originalNl);
|
|
127
142
|
const text = await provider.complete(`I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput}\n\nGive me the corrected command only.`, {
|
|
128
143
|
model: routing.smart, // always use smart model for fixes
|
|
129
144
|
maxTokens: 256,
|
|
130
|
-
system: buildSystemPrompt(perms,
|
|
145
|
+
system: buildSystemPrompt(perms, sessionEntries),
|
|
131
146
|
});
|
|
132
147
|
if (text.startsWith("BLOCKED:"))
|
|
133
148
|
throw new Error(text);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Cerebras provider — uses OpenAI-compatible API
|
|
2
2
|
// Default for open-source users. Fast inference on Llama models.
|
|
3
3
|
const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1";
|
|
4
|
-
const DEFAULT_MODEL = "
|
|
4
|
+
const DEFAULT_MODEL = "llama3.1-8b";
|
|
5
5
|
export class CerebrasProvider {
|
|
6
6
|
name = "cerebras";
|
|
7
7
|
apiKey;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef } from "react";
|
|
2
2
|
import { Box, Text, useInput, useApp } from "ink";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
|
-
import { translateToCommand, explainCommand, fixCommand, checkPermissions, isIrreversible } from "./ai.js";
|
|
4
|
+
import { translateToCommand, explainCommand, fixCommand, checkPermissions, isIrreversible, type SessionEntry } from "./ai.js";
|
|
5
5
|
import { loadHistory, appendHistory, loadConfig, saveConfig, type Permissions } from "./history.js";
|
|
6
6
|
import { loadCache } from "./cache.js";
|
|
7
7
|
import Onboarding from "./Onboarding.js";
|
|
@@ -39,7 +39,7 @@ interface TabState {
|
|
|
39
39
|
id: number;
|
|
40
40
|
cwd: string;
|
|
41
41
|
scroll: ScrollEntry[];
|
|
42
|
-
|
|
42
|
+
sessionEntries: SessionEntry[];
|
|
43
43
|
sessionNl: string[];
|
|
44
44
|
phase: Phase;
|
|
45
45
|
streamLines: string[];
|
|
@@ -77,7 +77,7 @@ function maybeCd(command: string): string | null {
|
|
|
77
77
|
function newTab(id: number, cwd: string): TabState {
|
|
78
78
|
return {
|
|
79
79
|
id, cwd,
|
|
80
|
-
scroll: [],
|
|
80
|
+
scroll: [], sessionEntries: [], sessionNl: [],
|
|
81
81
|
phase: { type: "input", value: "", cursor: 0, histIdx: -1, raw: false },
|
|
82
82
|
streamLines: [],
|
|
83
83
|
};
|
|
@@ -133,10 +133,13 @@ export default function App() {
|
|
|
133
133
|
const commitStream = (nl: string, cmd: string, lines: string[], error: boolean) => {
|
|
134
134
|
const truncated = lines.length > MAX_LINES;
|
|
135
135
|
const filePaths = !error ? extractFilePaths(lines) : [];
|
|
136
|
+
// Build short output summary for session context (first 10 lines)
|
|
137
|
+
const shortOutput = lines.slice(0, 10).join("\n") + (lines.length > 10 ? `\n... (${lines.length} lines total)` : "");
|
|
138
|
+
const entry: SessionEntry = { nl, cmd, output: shortOutput, error: error || undefined };
|
|
136
139
|
updateTab(t => ({
|
|
137
140
|
...t,
|
|
138
141
|
streamLines: [],
|
|
139
|
-
|
|
142
|
+
sessionEntries: [...t.sessionEntries.slice(-9), entry],
|
|
140
143
|
scroll: [...t.scroll, {
|
|
141
144
|
nl, cmd,
|
|
142
145
|
lines: truncated ? lines.slice(0, MAX_LINES) : lines,
|
|
@@ -192,10 +195,10 @@ export default function App() {
|
|
|
192
195
|
updateTab(t => ({ ...t, sessionNl: [...t.sessionNl, nl] }));
|
|
193
196
|
if (raw) { await runPhase(nl, nl, true); return; }
|
|
194
197
|
|
|
195
|
-
const
|
|
198
|
+
const sessionEntries = tabs[activeTab].sessionEntries;
|
|
196
199
|
setPhase({ type: "thinking", nl, partial: "" });
|
|
197
200
|
try {
|
|
198
|
-
const command = await translateToCommand(nl, config.permissions,
|
|
201
|
+
const command = await translateToCommand(nl, config.permissions, sessionEntries, partial =>
|
|
199
202
|
setPhase({ type: "thinking", nl, partial })
|
|
200
203
|
);
|
|
201
204
|
const blocked = checkPermissions(command, config.permissions);
|
|
@@ -346,7 +349,7 @@ export default function App() {
|
|
|
346
349
|
const { nl, command, errorOutput } = phase;
|
|
347
350
|
setPhase({ type: "thinking", nl, partial: "" });
|
|
348
351
|
try {
|
|
349
|
-
const fixed = await fixCommand(nl, command, errorOutput, config.permissions, tab.
|
|
352
|
+
const fixed = await fixCommand(nl, command, errorOutput, config.permissions, tab.sessionEntries);
|
|
350
353
|
const danger = isIrreversible(fixed);
|
|
351
354
|
if (!config.confirm && !danger) { await runPhase(nl, fixed, false); return; }
|
|
352
355
|
setPhase({ type: "confirm", nl, command: fixed, danger });
|
package/src/ai.ts
CHANGED
|
@@ -26,10 +26,10 @@ function pickModel(nl: string): { fast: string; smart: string; pick: "fast" | "s
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Cerebras —
|
|
29
|
+
// Cerebras — fast model for simple, smart model for complex
|
|
30
30
|
return {
|
|
31
|
-
fast: "
|
|
32
|
-
smart: "
|
|
31
|
+
fast: "llama3.1-8b",
|
|
32
|
+
smart: "llama3.1-8b",
|
|
33
33
|
pick: isComplex ? "smart" : "fast",
|
|
34
34
|
};
|
|
35
35
|
}
|
|
@@ -68,9 +68,18 @@ export function checkPermissions(command: string, perms: Permissions): string |
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ── session context ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export interface SessionEntry {
|
|
74
|
+
nl: string;
|
|
75
|
+
cmd: string;
|
|
76
|
+
output?: string; // short output (first few lines)
|
|
77
|
+
error?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
72
81
|
|
|
73
|
-
function buildSystemPrompt(perms: Permissions,
|
|
82
|
+
function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[]): string {
|
|
74
83
|
const restrictions: string[] = [];
|
|
75
84
|
if (!perms.destructive)
|
|
76
85
|
restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
|
|
@@ -87,11 +96,22 @@ function buildSystemPrompt(perms: Permissions, sessionCmds: string[]): string {
|
|
|
87
96
|
? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
|
|
88
97
|
: "";
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
:
|
|
99
|
+
let contextBlock = "";
|
|
100
|
+
if (sessionEntries.length > 0) {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
for (const e of sessionEntries.slice(-5)) { // last 5 interactions
|
|
103
|
+
lines.push(`> ${e.nl}`);
|
|
104
|
+
lines.push(`$ ${e.cmd}`);
|
|
105
|
+
if (e.output) lines.push(e.output);
|
|
106
|
+
if (e.error) lines.push("(command failed)");
|
|
107
|
+
}
|
|
108
|
+
contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
|
|
109
|
+
}
|
|
93
110
|
|
|
94
111
|
return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
|
|
112
|
+
The user describes what they want in plain English. You translate to the exact shell command.
|
|
113
|
+
Pay attention to session history — when the user says "inside X folder" they mean a folder visible in the previous output.
|
|
114
|
+
If the user refers to a relative path from a previous command, resolve it correctly.
|
|
95
115
|
cwd: ${process.cwd()}
|
|
96
116
|
shell: zsh / macOS${restrictionBlock}${contextBlock}`;
|
|
97
117
|
}
|
|
@@ -101,17 +121,19 @@ shell: zsh / macOS${restrictionBlock}${contextBlock}`;
|
|
|
101
121
|
export async function translateToCommand(
|
|
102
122
|
nl: string,
|
|
103
123
|
perms: Permissions,
|
|
104
|
-
|
|
124
|
+
sessionEntries: SessionEntry[],
|
|
105
125
|
onToken?: (partial: string) => void
|
|
106
126
|
): Promise<string> {
|
|
107
|
-
// cache
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
// Only use cache when there's no session context (context makes same NL produce different commands)
|
|
128
|
+
if (sessionEntries.length === 0) {
|
|
129
|
+
const cached = cacheGet(nl);
|
|
130
|
+
if (cached) { onToken?.(cached); return cached; }
|
|
131
|
+
}
|
|
110
132
|
|
|
111
133
|
const provider = getProvider();
|
|
112
134
|
const routing = pickModel(nl);
|
|
113
135
|
const model = routing.pick === "smart" ? routing.smart : routing.fast;
|
|
114
|
-
const system = buildSystemPrompt(perms,
|
|
136
|
+
const system = buildSystemPrompt(perms, sessionEntries);
|
|
115
137
|
|
|
116
138
|
let text: string;
|
|
117
139
|
|
|
@@ -133,10 +155,10 @@ export async function translateToCommand(
|
|
|
133
155
|
export function prefetchNext(
|
|
134
156
|
lastNl: string,
|
|
135
157
|
perms: Permissions,
|
|
136
|
-
|
|
158
|
+
sessionEntries: SessionEntry[]
|
|
137
159
|
) {
|
|
138
|
-
if (cacheGet(lastNl)) return;
|
|
139
|
-
translateToCommand(lastNl, perms,
|
|
160
|
+
if (sessionEntries.length === 0 && cacheGet(lastNl)) return;
|
|
161
|
+
translateToCommand(lastNl, perms, sessionEntries).catch(() => {});
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
// ── explain ───────────────────────────────────────────────────────────────────
|
|
@@ -158,7 +180,7 @@ export async function fixCommand(
|
|
|
158
180
|
failedCommand: string,
|
|
159
181
|
errorOutput: string,
|
|
160
182
|
perms: Permissions,
|
|
161
|
-
|
|
183
|
+
sessionEntries: SessionEntry[]
|
|
162
184
|
): Promise<string> {
|
|
163
185
|
const provider = getProvider();
|
|
164
186
|
const routing = pickModel(originalNl);
|
|
@@ -167,7 +189,7 @@ export async function fixCommand(
|
|
|
167
189
|
{
|
|
168
190
|
model: routing.smart, // always use smart model for fixes
|
|
169
191
|
maxTokens: 256,
|
|
170
|
-
system: buildSystemPrompt(perms,
|
|
192
|
+
system: buildSystemPrompt(perms, sessionEntries),
|
|
171
193
|
}
|
|
172
194
|
);
|
|
173
195
|
if (text.startsWith("BLOCKED:")) throw new Error(text);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { LLMProvider, ProviderOptions, StreamCallbacks } from "./base.js";
|
|
5
5
|
|
|
6
6
|
const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1";
|
|
7
|
-
const DEFAULT_MODEL = "
|
|
7
|
+
const DEFAULT_MODEL = "llama3.1-8b";
|
|
8
8
|
|
|
9
9
|
export class CerebrasProvider implements LLMProvider {
|
|
10
10
|
readonly name = "cerebras";
|