@hasna/terminal 0.2.1 → 0.2.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/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: [], sessionCmds: [], sessionNl: [],
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
- sessionCmds: [...t.sessionCmds.slice(-9), cmd],
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 sessionCmds = tabs[activeTab].sessionCmds;
143
+ const sessionEntries = tabs[activeTab].sessionEntries;
141
144
  setPhase({ type: "thinking", nl, partial: "" });
142
145
  try {
143
- const command = await translateToCommand(nl, config.permissions, sessionCmds, partial => setPhase({ type: "thinking", nl, partial }));
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.sessionCmds);
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
@@ -7,6 +7,8 @@ const COMPLEX_SIGNALS = [
7
7
  /\b(all files?|recursively|bulk|batch)\b/i,
8
8
  /\b(pipeline|chain|then|and then|after)\b/i,
9
9
  /\b(if|when|unless|only if)\b/i,
10
+ /\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i, // multi-step navigation
11
+ /\b(inside|within|under)\b/i, // relative references need context awareness
10
12
  /[|&;]{2}/, // pipes / && in NL (unusual = complex intent)
11
13
  ];
12
14
  /** Model routing per provider */
@@ -20,10 +22,10 @@ function pickModel(nl) {
20
22
  pick: isComplex ? "smart" : "fast",
21
23
  };
22
24
  }
23
- // Cerebras — fast model for simple, smart model for complex
25
+ // Cerebras — llama for simple, qwen for complex
24
26
  return {
25
27
  fast: "llama3.1-8b",
26
- smart: "llama3.1-8b",
28
+ smart: "qwen-3-235b-a22b-instruct-2507",
27
29
  pick: isComplex ? "smart" : "fast",
28
30
  };
29
31
  }
@@ -56,7 +58,7 @@ export function checkPermissions(command, perms) {
56
58
  return null;
57
59
  }
58
60
  // ── system prompt ─────────────────────────────────────────────────────────────
59
- function buildSystemPrompt(perms, sessionCmds) {
61
+ function buildSystemPrompt(perms, sessionEntries) {
60
62
  const restrictions = [];
61
63
  if (!perms.destructive)
62
64
  restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
@@ -71,25 +73,40 @@ function buildSystemPrompt(perms, sessionCmds) {
71
73
  const restrictionBlock = restrictions.length > 0
72
74
  ? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
73
75
  : "";
74
- const contextBlock = sessionCmds.length > 0
75
- ? `\n\nSESSION HISTORY:\n${sessionCmds.map((c) => `$ ${c}`).join("\n")}`
76
- : "";
76
+ let contextBlock = "";
77
+ if (sessionEntries.length > 0) {
78
+ const lines = [];
79
+ for (const e of sessionEntries.slice(-5)) { // last 5 interactions
80
+ lines.push(`> ${e.nl}`);
81
+ lines.push(`$ ${e.cmd}`);
82
+ if (e.output)
83
+ lines.push(e.output);
84
+ if (e.error)
85
+ lines.push("(command failed)");
86
+ }
87
+ contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
88
+ }
77
89
  return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
90
+ The user describes what they want in plain English. You translate to the exact shell command.
91
+ Pay attention to session history — when the user says "inside X folder" they mean a folder visible in the previous output.
92
+ If the user refers to a relative path from a previous command, resolve it correctly.
78
93
  cwd: ${process.cwd()}
79
94
  shell: zsh / macOS${restrictionBlock}${contextBlock}`;
80
95
  }
81
96
  // ── streaming translate ───────────────────────────────────────────────────────
82
- export async function translateToCommand(nl, perms, sessionCmds, onToken) {
83
- // cache hit instant
84
- const cached = cacheGet(nl);
85
- if (cached) {
86
- onToken?.(cached);
87
- return cached;
97
+ export async function translateToCommand(nl, perms, sessionEntries, onToken) {
98
+ // Only use cache when there's no session context (context makes same NL produce different commands)
99
+ if (sessionEntries.length === 0) {
100
+ const cached = cacheGet(nl);
101
+ if (cached) {
102
+ onToken?.(cached);
103
+ return cached;
104
+ }
88
105
  }
89
106
  const provider = getProvider();
90
107
  const routing = pickModel(nl);
91
108
  const model = routing.pick === "smart" ? routing.smart : routing.fast;
92
- const system = buildSystemPrompt(perms, sessionCmds);
109
+ const system = buildSystemPrompt(perms, sessionEntries);
93
110
  let text;
94
111
  if (onToken) {
95
112
  text = await provider.stream(nl, { model, maxTokens: 256, system }, {
@@ -105,10 +122,10 @@ export async function translateToCommand(nl, perms, sessionCmds, onToken) {
105
122
  return text;
106
123
  }
107
124
  // ── prefetch ──────────────────────────────────────────────────────────────────
108
- export function prefetchNext(lastNl, perms, sessionCmds) {
109
- if (cacheGet(lastNl))
125
+ export function prefetchNext(lastNl, perms, sessionEntries) {
126
+ if (sessionEntries.length === 0 && cacheGet(lastNl))
110
127
  return;
111
- translateToCommand(lastNl, perms, sessionCmds).catch(() => { });
128
+ translateToCommand(lastNl, perms, sessionEntries).catch(() => { });
112
129
  }
113
130
  // ── explain ───────────────────────────────────────────────────────────────────
114
131
  export async function explainCommand(command) {
@@ -121,13 +138,13 @@ export async function explainCommand(command) {
121
138
  });
122
139
  }
123
140
  // ── auto-fix ──────────────────────────────────────────────────────────────────
124
- export async function fixCommand(originalNl, failedCommand, errorOutput, perms, sessionCmds) {
141
+ export async function fixCommand(originalNl, failedCommand, errorOutput, perms, sessionEntries) {
125
142
  const provider = getProvider();
126
143
  const routing = pickModel(originalNl);
127
144
  const text = await provider.complete(`I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput}\n\nGive me the corrected command only.`, {
128
145
  model: routing.smart, // always use smart model for fixes
129
146
  maxTokens: 256,
130
- system: buildSystemPrompt(perms, sessionCmds),
147
+ system: buildSystemPrompt(perms, sessionEntries),
131
148
  });
132
149
  if (text.startsWith("BLOCKED:"))
133
150
  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 = "llama3.1-8b";
4
+ const DEFAULT_MODEL = "qwen-3-235b-a22b-instruct-2507";
5
5
  export class CerebrasProvider {
6
6
  name = "cerebras";
7
7
  apiKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "0.2.1",
3
+ "version": "0.2.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/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
- sessionCmds: string[];
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: [], sessionCmds: [], sessionNl: [],
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
- sessionCmds: [...t.sessionCmds.slice(-9), cmd],
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 sessionCmds = tabs[activeTab].sessionCmds;
198
+ const sessionEntries = tabs[activeTab].sessionEntries;
196
199
  setPhase({ type: "thinking", nl, partial: "" });
197
200
  try {
198
- const command = await translateToCommand(nl, config.permissions, sessionCmds, partial =>
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.sessionCmds);
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
@@ -10,6 +10,8 @@ const COMPLEX_SIGNALS = [
10
10
  /\b(all files?|recursively|bulk|batch)\b/i,
11
11
  /\b(pipeline|chain|then|and then|after)\b/i,
12
12
  /\b(if|when|unless|only if)\b/i,
13
+ /\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i, // multi-step navigation
14
+ /\b(inside|within|under)\b/i, // relative references need context awareness
13
15
  /[|&;]{2}/, // pipes / && in NL (unusual = complex intent)
14
16
  ];
15
17
 
@@ -26,10 +28,10 @@ function pickModel(nl: string): { fast: string; smart: string; pick: "fast" | "s
26
28
  };
27
29
  }
28
30
 
29
- // Cerebras — fast model for simple, smart model for complex
31
+ // Cerebras — llama for simple, qwen for complex
30
32
  return {
31
33
  fast: "llama3.1-8b",
32
- smart: "llama3.1-8b",
34
+ smart: "qwen-3-235b-a22b-instruct-2507",
33
35
  pick: isComplex ? "smart" : "fast",
34
36
  };
35
37
  }
@@ -68,9 +70,18 @@ export function checkPermissions(command: string, perms: Permissions): string |
68
70
  return null;
69
71
  }
70
72
 
73
+ // ── session context ──────────────────────────────────────────────────────────
74
+
75
+ export interface SessionEntry {
76
+ nl: string;
77
+ cmd: string;
78
+ output?: string; // short output (first few lines)
79
+ error?: boolean;
80
+ }
81
+
71
82
  // ── system prompt ─────────────────────────────────────────────────────────────
72
83
 
73
- function buildSystemPrompt(perms: Permissions, sessionCmds: string[]): string {
84
+ function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[]): string {
74
85
  const restrictions: string[] = [];
75
86
  if (!perms.destructive)
76
87
  restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
@@ -87,11 +98,22 @@ function buildSystemPrompt(perms: Permissions, sessionCmds: string[]): string {
87
98
  ? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
88
99
  : "";
89
100
 
90
- const contextBlock = sessionCmds.length > 0
91
- ? `\n\nSESSION HISTORY:\n${sessionCmds.map((c) => `$ ${c}`).join("\n")}`
92
- : "";
101
+ let contextBlock = "";
102
+ if (sessionEntries.length > 0) {
103
+ const lines: string[] = [];
104
+ for (const e of sessionEntries.slice(-5)) { // last 5 interactions
105
+ lines.push(`> ${e.nl}`);
106
+ lines.push(`$ ${e.cmd}`);
107
+ if (e.output) lines.push(e.output);
108
+ if (e.error) lines.push("(command failed)");
109
+ }
110
+ contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
111
+ }
93
112
 
94
113
  return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
114
+ The user describes what they want in plain English. You translate to the exact shell command.
115
+ Pay attention to session history — when the user says "inside X folder" they mean a folder visible in the previous output.
116
+ If the user refers to a relative path from a previous command, resolve it correctly.
95
117
  cwd: ${process.cwd()}
96
118
  shell: zsh / macOS${restrictionBlock}${contextBlock}`;
97
119
  }
@@ -101,17 +123,19 @@ shell: zsh / macOS${restrictionBlock}${contextBlock}`;
101
123
  export async function translateToCommand(
102
124
  nl: string,
103
125
  perms: Permissions,
104
- sessionCmds: string[],
126
+ sessionEntries: SessionEntry[],
105
127
  onToken?: (partial: string) => void
106
128
  ): Promise<string> {
107
- // cache hit instant
108
- const cached = cacheGet(nl);
109
- if (cached) { onToken?.(cached); return cached; }
129
+ // Only use cache when there's no session context (context makes same NL produce different commands)
130
+ if (sessionEntries.length === 0) {
131
+ const cached = cacheGet(nl);
132
+ if (cached) { onToken?.(cached); return cached; }
133
+ }
110
134
 
111
135
  const provider = getProvider();
112
136
  const routing = pickModel(nl);
113
137
  const model = routing.pick === "smart" ? routing.smart : routing.fast;
114
- const system = buildSystemPrompt(perms, sessionCmds);
138
+ const system = buildSystemPrompt(perms, sessionEntries);
115
139
 
116
140
  let text: string;
117
141
 
@@ -133,10 +157,10 @@ export async function translateToCommand(
133
157
  export function prefetchNext(
134
158
  lastNl: string,
135
159
  perms: Permissions,
136
- sessionCmds: string[]
160
+ sessionEntries: SessionEntry[]
137
161
  ) {
138
- if (cacheGet(lastNl)) return;
139
- translateToCommand(lastNl, perms, sessionCmds).catch(() => {});
162
+ if (sessionEntries.length === 0 && cacheGet(lastNl)) return;
163
+ translateToCommand(lastNl, perms, sessionEntries).catch(() => {});
140
164
  }
141
165
 
142
166
  // ── explain ───────────────────────────────────────────────────────────────────
@@ -158,7 +182,7 @@ export async function fixCommand(
158
182
  failedCommand: string,
159
183
  errorOutput: string,
160
184
  perms: Permissions,
161
- sessionCmds: string[]
185
+ sessionEntries: SessionEntry[]
162
186
  ): Promise<string> {
163
187
  const provider = getProvider();
164
188
  const routing = pickModel(originalNl);
@@ -167,7 +191,7 @@ export async function fixCommand(
167
191
  {
168
192
  model: routing.smart, // always use smart model for fixes
169
193
  maxTokens: 256,
170
- system: buildSystemPrompt(perms, sessionCmds),
194
+ system: buildSystemPrompt(perms, sessionEntries),
171
195
  }
172
196
  );
173
197
  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 = "llama3.1-8b";
7
+ const DEFAULT_MODEL = "qwen-3-235b-a22b-instruct-2507";
8
8
 
9
9
  export class CerebrasProvider implements LLMProvider {
10
10
  readonly name = "cerebras";