@hasna/terminal 0.2.1 → 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 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
@@ -56,7 +56,7 @@ export function checkPermissions(command, perms) {
56
56
  return null;
57
57
  }
58
58
  // ── system prompt ─────────────────────────────────────────────────────────────
59
- function buildSystemPrompt(perms, sessionCmds) {
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
- const contextBlock = sessionCmds.length > 0
75
- ? `\n\nSESSION HISTORY:\n${sessionCmds.map((c) => `$ ${c}`).join("\n")}`
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, sessionCmds, onToken) {
83
- // cache hit instant
84
- const cached = cacheGet(nl);
85
- if (cached) {
86
- onToken?.(cached);
87
- return cached;
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, sessionCmds);
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, sessionCmds) {
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, sessionCmds).catch(() => { });
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, sessionCmds) {
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, sessionCmds),
145
+ system: buildSystemPrompt(perms, sessionEntries),
131
146
  });
132
147
  if (text.startsWith("BLOCKED:"))
133
148
  throw new Error(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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
@@ -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, sessionCmds: string[]): string {
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
- const contextBlock = sessionCmds.length > 0
91
- ? `\n\nSESSION HISTORY:\n${sessionCmds.map((c) => `$ ${c}`).join("\n")}`
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
- sessionCmds: string[],
124
+ sessionEntries: SessionEntry[],
105
125
  onToken?: (partial: string) => void
106
126
  ): Promise<string> {
107
- // cache hit instant
108
- const cached = cacheGet(nl);
109
- if (cached) { onToken?.(cached); return cached; }
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, sessionCmds);
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
- sessionCmds: string[]
158
+ sessionEntries: SessionEntry[]
137
159
  ) {
138
- if (cacheGet(lastNl)) return;
139
- translateToCommand(lastNl, perms, sessionCmds).catch(() => {});
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
- sessionCmds: string[]
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, sessionCmds),
192
+ system: buildSystemPrompt(perms, sessionEntries),
171
193
  }
172
194
  );
173
195
  if (text.startsWith("BLOCKED:")) throw new Error(text);