@hasna/terminal 0.1.0 → 0.1.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 +163 -38
- package/dist/Onboarding.js +19 -23
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +45 -0
- package/dist/ai.js +73 -23
- package/dist/history.js +4 -4
- package/package.json +4 -4
- package/src/App.tsx +262 -82
- package/src/Onboarding.tsx +47 -59
- package/src/Spinner.tsx +24 -0
- package/src/StatusBar.tsx +67 -0
- package/src/ai.ts +94 -27
- package/src/history.ts +4 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { type Permissions } from "./history.js";
|
|
6
|
+
|
|
7
|
+
function getCwd(): string {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const home = homedir();
|
|
10
|
+
return cwd.startsWith(home) ? "~" + cwd.slice(home.length) : cwd;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getGitBranch(): string | null {
|
|
14
|
+
try {
|
|
15
|
+
return execSync("git branch --show-current 2>/dev/null", { stdio: ["ignore", "pipe", "ignore"] })
|
|
16
|
+
.toString()
|
|
17
|
+
.trim() || null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getGitDirty(): boolean {
|
|
24
|
+
try {
|
|
25
|
+
const out = execSync("git status --porcelain 2>/dev/null", { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
26
|
+
return out.trim().length > 0;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function activePerms(perms: Permissions): string[] {
|
|
33
|
+
const labels: Array<[keyof Permissions, string]> = [
|
|
34
|
+
["destructive", "del"],
|
|
35
|
+
["network", "net"],
|
|
36
|
+
["sudo", "sudo"],
|
|
37
|
+
["install", "pkg"],
|
|
38
|
+
["write_outside_cwd", "write"],
|
|
39
|
+
];
|
|
40
|
+
return labels.filter(([k]) => perms[k]).map(([, l]) => l);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
permissions: Permissions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function StatusBar({ permissions }: Props) {
|
|
48
|
+
const cwd = getCwd();
|
|
49
|
+
const branch = getGitBranch();
|
|
50
|
+
const dirty = branch ? getGitDirty() : false;
|
|
51
|
+
const perms = activePerms(permissions);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Box gap={2} paddingLeft={2} marginTop={1}>
|
|
55
|
+
<Text dimColor>{cwd}</Text>
|
|
56
|
+
{branch && (
|
|
57
|
+
<Text dimColor>
|
|
58
|
+
{branch}
|
|
59
|
+
{dirty ? " ●" : ""}
|
|
60
|
+
</Text>
|
|
61
|
+
)}
|
|
62
|
+
{perms.length > 0 && (
|
|
63
|
+
<Text dimColor>{perms.join(" · ")}</Text>
|
|
64
|
+
)}
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
}
|
package/src/ai.ts
CHANGED
|
@@ -3,7 +3,43 @@ import type { Permissions } from "./history.js";
|
|
|
3
3
|
|
|
4
4
|
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// ── irreversibility ───────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const IRREVERSIBLE_PATTERNS = [
|
|
9
|
+
/\brm\s/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i,
|
|
10
|
+
/\bdelete\s+from\b/i, /\bmv\b.*\/dev\/null/, /\b>\s*[^>]/, // overwrite redirect
|
|
11
|
+
/\bdd\b/, /\bmkfs\b/, /\bformat\b/, /\bshred\b/,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function isIrreversible(command: string): boolean {
|
|
15
|
+
return IRREVERSIBLE_PATTERNS.some((r) => r.test(command));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── permissions ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const DESTRUCTIVE_PATTERNS = [/\brm\b/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i, /\bdelete\s+from\b/i];
|
|
21
|
+
const NETWORK_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bssh\b/, /\bscp\b/, /\bping\b/, /\bnc\b/, /\bnetcat\b/];
|
|
22
|
+
const SUDO_PATTERNS = [/\bsudo\b/];
|
|
23
|
+
const INSTALL_PATTERNS = [/\bbrew\s+install\b/, /\bnpm\s+install\s+-g\b/, /\bpip\s+install\b/, /\bapt\s+install\b/, /\byum\s+install\b/];
|
|
24
|
+
const WRITE_OUTSIDE_PATTERNS = [/\s(\/etc|\/usr|\/var|\/opt|\/root|~\/[^.])/, />\s*\//];
|
|
25
|
+
|
|
26
|
+
export function checkPermissions(command: string, perms: Permissions): string | null {
|
|
27
|
+
if (!perms.destructive && DESTRUCTIVE_PATTERNS.some((r) => r.test(command)))
|
|
28
|
+
return "destructive commands are disabled";
|
|
29
|
+
if (!perms.network && NETWORK_PATTERNS.some((r) => r.test(command)))
|
|
30
|
+
return "network commands are disabled";
|
|
31
|
+
if (!perms.sudo && SUDO_PATTERNS.some((r) => r.test(command)))
|
|
32
|
+
return "sudo is disabled";
|
|
33
|
+
if (!perms.install && INSTALL_PATTERNS.some((r) => r.test(command)))
|
|
34
|
+
return "package installation is disabled";
|
|
35
|
+
if (!perms.write_outside_cwd && WRITE_OUTSIDE_PATTERNS.some((r) => r.test(command)))
|
|
36
|
+
return "writing outside cwd is disabled";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function buildSystemPrompt(perms: Permissions, sessionCmds: string[]): string {
|
|
7
43
|
const restrictions: string[] = [];
|
|
8
44
|
if (!perms.destructive)
|
|
9
45
|
restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data (rm, rmdir, truncate, DROP TABLE, etc.)");
|
|
@@ -16,47 +52,78 @@ function buildSystemPrompt(perms: Permissions): string {
|
|
|
16
52
|
if (!perms.install)
|
|
17
53
|
restrictions.push("- NEVER generate commands that install packages (brew install, npm install -g, pip install, apt install, etc.)");
|
|
18
54
|
|
|
19
|
-
const restrictionBlock =
|
|
20
|
-
restrictions.
|
|
21
|
-
|
|
22
|
-
|
|
55
|
+
const restrictionBlock = restrictions.length > 0
|
|
56
|
+
? `\n\nCURRENT RESTRICTIONS (respect absolutely):\n${restrictions.join("\n")}\nIf restricted, output exactly: BLOCKED: <reason>`
|
|
57
|
+
: "";
|
|
58
|
+
|
|
59
|
+
const contextBlock = sessionCmds.length > 0
|
|
60
|
+
? `\n\nRECENT COMMANDS THIS SESSION (for context — e.g. "undo that", "do the same for X"):\n${sessionCmds.map((c) => `$ ${c}`).join("\n")}`
|
|
61
|
+
: "";
|
|
62
|
+
|
|
63
|
+
const cwd = process.cwd();
|
|
23
64
|
|
|
24
65
|
return `You are a terminal assistant. The user will describe what they want to do in plain English.
|
|
25
66
|
Your job is to output ONLY the exact shell command(s) to accomplish this — nothing else.
|
|
26
67
|
No explanation. No markdown. No backticks. Just the raw command.
|
|
27
68
|
If multiple commands are needed, join them with && or use a newline.
|
|
28
|
-
Assume macOS/Linux zsh environment
|
|
69
|
+
Assume macOS/Linux zsh environment.
|
|
70
|
+
Current working directory: ${cwd}${restrictionBlock}${contextBlock}`;
|
|
29
71
|
}
|
|
30
72
|
|
|
31
|
-
|
|
32
|
-
const DESTRUCTIVE_PATTERNS = [/\brm\b/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i, /\bdelete\s+from\b/i];
|
|
33
|
-
const NETWORK_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bssh\b/, /\bscp\b/, /\bping\b/, /\bnc\b/, /\bnetcat\b/];
|
|
34
|
-
const SUDO_PATTERNS = [/\bsudo\b/];
|
|
35
|
-
const INSTALL_PATTERNS = [/\bbrew\s+install\b/, /\bnpm\s+install\s+-g\b/, /\bpip\s+install\b/, /\bapt\s+install\b/, /\byum\s+install\b/];
|
|
36
|
-
const WRITE_OUTSIDE_PATTERNS = [/\s(\/etc|\/usr|\/var|\/opt|\/root|~\/[^.])/, />\s*\//];
|
|
73
|
+
// ── explain ───────────────────────────────────────────────────────────────────
|
|
37
74
|
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return "writing outside cwd is disabled in your permissions";
|
|
49
|
-
return null;
|
|
75
|
+
export async function explainCommand(command: string): Promise<string> {
|
|
76
|
+
const message = await client.messages.create({
|
|
77
|
+
model: "claude-haiku-4-5-20251001",
|
|
78
|
+
max_tokens: 128,
|
|
79
|
+
system: "Explain what this shell command does in one plain English sentence. No markdown. No code blocks. Just a sentence.",
|
|
80
|
+
messages: [{ role: "user", content: command }],
|
|
81
|
+
});
|
|
82
|
+
const block = message.content[0];
|
|
83
|
+
if (block.type !== "text") return "";
|
|
84
|
+
return block.text.trim();
|
|
50
85
|
}
|
|
51
86
|
|
|
52
|
-
|
|
87
|
+
// ── auto-fix ──────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export async function fixCommand(
|
|
90
|
+
originalNl: string,
|
|
91
|
+
failedCommand: string,
|
|
92
|
+
errorOutput: string,
|
|
93
|
+
perms: Permissions,
|
|
94
|
+
sessionCmds: string[]
|
|
95
|
+
): Promise<string> {
|
|
53
96
|
const message = await client.messages.create({
|
|
54
97
|
model: "claude-opus-4-6",
|
|
55
98
|
max_tokens: 256,
|
|
56
|
-
system: buildSystemPrompt(perms),
|
|
57
|
-
messages: [
|
|
99
|
+
system: buildSystemPrompt(perms, sessionCmds),
|
|
100
|
+
messages: [
|
|
101
|
+
{
|
|
102
|
+
role: "user",
|
|
103
|
+
content: `I wanted to: ${originalNl}\nI ran: ${failedCommand}\nIt failed with:\n${errorOutput}\n\nGive me the corrected command.`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
58
106
|
});
|
|
107
|
+
const block = message.content[0];
|
|
108
|
+
if (block.type !== "text") throw new Error("Unexpected response type");
|
|
109
|
+
const text = block.text.trim();
|
|
110
|
+
if (text.startsWith("BLOCKED:")) throw new Error(text);
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── translate ─────────────────────────────────────────────────────────────────
|
|
59
115
|
|
|
116
|
+
export async function translateToCommand(
|
|
117
|
+
nl: string,
|
|
118
|
+
perms: Permissions,
|
|
119
|
+
sessionCmds: string[]
|
|
120
|
+
): Promise<string> {
|
|
121
|
+
const message = await client.messages.create({
|
|
122
|
+
model: "claude-opus-4-6",
|
|
123
|
+
max_tokens: 256,
|
|
124
|
+
system: buildSystemPrompt(perms, sessionCmds),
|
|
125
|
+
messages: [{ role: "user", content: nl }],
|
|
126
|
+
});
|
|
60
127
|
const block = message.content[0];
|
|
61
128
|
if (block.type !== "text") throw new Error("Unexpected response type");
|
|
62
129
|
const text = block.text.trim();
|
package/src/history.ts
CHANGED
|
@@ -33,11 +33,11 @@ export interface Config {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export const DEFAULT_PERMISSIONS: Permissions = {
|
|
36
|
-
destructive:
|
|
36
|
+
destructive: true,
|
|
37
37
|
network: true,
|
|
38
|
-
sudo:
|
|
39
|
-
write_outside_cwd:
|
|
40
|
-
install:
|
|
38
|
+
sudo: true,
|
|
39
|
+
write_outside_cwd: true,
|
|
40
|
+
install: true,
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export const DEFAULT_CONFIG: Config = {
|