@hasna/terminal 0.5.0 → 0.5.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 +5 -1
- package/dist/cli.js +1 -1
- package/dist/recipes/storage.js +18 -0
- package/dist/sessions-db.js +6 -3
- package/package.json +1 -3
- package/src/App.tsx +5 -1
- package/src/cli.tsx +1 -1
- package/src/recipes/storage.ts +16 -0
- package/src/sessions-db.ts +8 -5
package/dist/App.js
CHANGED
|
@@ -69,7 +69,7 @@ export default function App() {
|
|
|
69
69
|
const [activeTab, setActiveTab] = useState(0);
|
|
70
70
|
const abortRef = useRef(null);
|
|
71
71
|
let nextTabId = useRef(2);
|
|
72
|
-
const sessionIdRef = useRef(
|
|
72
|
+
const sessionIdRef = useRef("");
|
|
73
73
|
const interactionIdRef = useRef(0);
|
|
74
74
|
const tab = tabs[activeTab];
|
|
75
75
|
const allNl = [...nlHistory, ...tab.sessionNl];
|
|
@@ -158,6 +158,10 @@ export default function App() {
|
|
|
158
158
|
// ── translate + run ─────────────────────────────────────────────────────────
|
|
159
159
|
const translateAndRun = async (nl, raw) => {
|
|
160
160
|
updateTab(t => ({ ...t, sessionNl: [...t.sessionNl, nl] }));
|
|
161
|
+
// Lazy session creation — only when user actually types something
|
|
162
|
+
if (!sessionIdRef.current) {
|
|
163
|
+
sessionIdRef.current = createSession(process.cwd());
|
|
164
|
+
}
|
|
161
165
|
// Log interaction start
|
|
162
166
|
const startTime = Date.now();
|
|
163
167
|
interactionIdRef.current = logInteraction(sessionIdRef.current, { nl });
|
package/dist/cli.js
CHANGED
package/dist/recipes/storage.js
CHANGED
|
@@ -45,6 +45,20 @@ export function getRecipe(name, projectPath) {
|
|
|
45
45
|
export function createRecipe(opts) {
|
|
46
46
|
const filePath = opts.project ? projectFile(opts.project) : GLOBAL_FILE;
|
|
47
47
|
const store = loadStore(filePath);
|
|
48
|
+
// Prevent duplicates — update existing if same name
|
|
49
|
+
const existingIdx = store.recipes.findIndex(r => r.name === opts.name);
|
|
50
|
+
if (existingIdx >= 0) {
|
|
51
|
+
store.recipes[existingIdx].command = opts.command;
|
|
52
|
+
store.recipes[existingIdx].updatedAt = Date.now();
|
|
53
|
+
if (opts.description)
|
|
54
|
+
store.recipes[existingIdx].description = opts.description;
|
|
55
|
+
if (opts.tags)
|
|
56
|
+
store.recipes[existingIdx].tags = opts.tags;
|
|
57
|
+
if (opts.collection)
|
|
58
|
+
store.recipes[existingIdx].collection = opts.collection;
|
|
59
|
+
saveStore(filePath, store);
|
|
60
|
+
return store.recipes[existingIdx];
|
|
61
|
+
}
|
|
48
62
|
// Auto-detect variables from command if not explicitly provided
|
|
49
63
|
const detectedVars = extractVariables(opts.command);
|
|
50
64
|
const variables = opts.variables ?? detectedVars.map(name => ({ name, required: true }));
|
|
@@ -98,6 +112,10 @@ export function listCollections(projectPath) {
|
|
|
98
112
|
export function createCollection(opts) {
|
|
99
113
|
const filePath = opts.project ? projectFile(opts.project) : GLOBAL_FILE;
|
|
100
114
|
const store = loadStore(filePath);
|
|
115
|
+
// Prevent duplicates — return existing if same name
|
|
116
|
+
const existing = store.collections.find(c => c.name === opts.name);
|
|
117
|
+
if (existing)
|
|
118
|
+
return existing;
|
|
101
119
|
const collection = {
|
|
102
120
|
id: genId(),
|
|
103
121
|
name: opts.name,
|
package/dist/sessions-db.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SQLite session database — tracks every terminal interaction
|
|
2
|
-
|
|
2
|
+
// @ts-ignore — bun:sqlite is a bun built-in
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
3
4
|
import { existsSync, mkdirSync } from "fs";
|
|
4
5
|
import { homedir } from "os";
|
|
5
6
|
import { join } from "path";
|
|
@@ -13,7 +14,7 @@ function getDb() {
|
|
|
13
14
|
if (!existsSync(DIR))
|
|
14
15
|
mkdirSync(DIR, { recursive: true });
|
|
15
16
|
db = new Database(DB_PATH);
|
|
16
|
-
db.
|
|
17
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
17
18
|
db.exec(`
|
|
18
19
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
19
20
|
id TEXT PRIMARY KEY,
|
|
@@ -62,7 +63,9 @@ export function getSession(id) {
|
|
|
62
63
|
export function logInteraction(sessionId, data) {
|
|
63
64
|
const result = getDb().prepare(`INSERT INTO interactions (session_id, nl, command, output, exit_code, tokens_used, tokens_saved, duration_ms, model, cached, created_at)
|
|
64
65
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(sessionId, data.nl, data.command ?? null, data.output ? data.output.slice(0, 500) : null, data.exitCode ?? null, data.tokensUsed ?? 0, data.tokensSaved ?? 0, data.durationMs ?? null, data.model ?? null, data.cached ? 1 : 0, Date.now());
|
|
65
|
-
|
|
66
|
+
// bun:sqlite — lastInsertRowid is a property on the statement after run()
|
|
67
|
+
const lastId = getDb().prepare("SELECT last_insert_rowid() as id").get();
|
|
68
|
+
return lastId?.id ?? 0;
|
|
66
69
|
}
|
|
67
70
|
export function updateInteraction(id, data) {
|
|
68
71
|
const sets = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/terminal",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
19
19
|
"@typescript/vfs": "^1.6.4",
|
|
20
|
-
"better-sqlite3": "^12.8.0",
|
|
21
20
|
"ink": "^5.0.1",
|
|
22
21
|
"react": "^18.2.0",
|
|
23
22
|
"zod": "^4.3.6"
|
|
@@ -31,7 +30,6 @@
|
|
|
31
30
|
"url": "git+https://github.com/hasna/terminal.git"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
35
33
|
"@types/node": "^20.0.0",
|
|
36
34
|
"@types/react": "^18.2.0",
|
|
37
35
|
"tsx": "^4.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -110,7 +110,7 @@ export default function App() {
|
|
|
110
110
|
const [activeTab, setActiveTab] = useState(0);
|
|
111
111
|
const abortRef = useRef<AbortController | null>(null);
|
|
112
112
|
let nextTabId = useRef(2);
|
|
113
|
-
const sessionIdRef = useRef<string>(
|
|
113
|
+
const sessionIdRef = useRef<string>("");
|
|
114
114
|
const interactionIdRef = useRef<number>(0);
|
|
115
115
|
|
|
116
116
|
const tab = tabs[activeTab];
|
|
@@ -217,6 +217,10 @@ export default function App() {
|
|
|
217
217
|
const translateAndRun = async (nl: string, raw: boolean) => {
|
|
218
218
|
updateTab(t => ({ ...t, sessionNl: [...t.sessionNl, nl] }));
|
|
219
219
|
|
|
220
|
+
// Lazy session creation — only when user actually types something
|
|
221
|
+
if (!sessionIdRef.current) {
|
|
222
|
+
sessionIdRef.current = createSession(process.cwd());
|
|
223
|
+
}
|
|
220
224
|
// Log interaction start
|
|
221
225
|
const startTime = Date.now();
|
|
222
226
|
interactionIdRef.current = logInteraction(sessionIdRef.current, { nl });
|
package/src/cli.tsx
CHANGED
package/src/recipes/storage.ts
CHANGED
|
@@ -61,6 +61,18 @@ export function createRecipe(opts: {
|
|
|
61
61
|
const filePath = opts.project ? projectFile(opts.project) : GLOBAL_FILE;
|
|
62
62
|
const store = loadStore(filePath);
|
|
63
63
|
|
|
64
|
+
// Prevent duplicates — update existing if same name
|
|
65
|
+
const existingIdx = store.recipes.findIndex(r => r.name === opts.name);
|
|
66
|
+
if (existingIdx >= 0) {
|
|
67
|
+
store.recipes[existingIdx].command = opts.command;
|
|
68
|
+
store.recipes[existingIdx].updatedAt = Date.now();
|
|
69
|
+
if (opts.description) store.recipes[existingIdx].description = opts.description;
|
|
70
|
+
if (opts.tags) store.recipes[existingIdx].tags = opts.tags;
|
|
71
|
+
if (opts.collection) store.recipes[existingIdx].collection = opts.collection;
|
|
72
|
+
saveStore(filePath, store);
|
|
73
|
+
return store.recipes[existingIdx];
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
// Auto-detect variables from command if not explicitly provided
|
|
65
77
|
const detectedVars = extractVariables(opts.command);
|
|
66
78
|
const variables = opts.variables ?? detectedVars.map(name => ({ name, required: true }));
|
|
@@ -120,6 +132,10 @@ export function createCollection(opts: { name: string; description?: string; pro
|
|
|
120
132
|
const filePath = opts.project ? projectFile(opts.project) : GLOBAL_FILE;
|
|
121
133
|
const store = loadStore(filePath);
|
|
122
134
|
|
|
135
|
+
// Prevent duplicates — return existing if same name
|
|
136
|
+
const existing = store.collections.find(c => c.name === opts.name);
|
|
137
|
+
if (existing) return existing;
|
|
138
|
+
|
|
123
139
|
const collection: Collection = {
|
|
124
140
|
id: genId(),
|
|
125
141
|
name: opts.name,
|
package/src/sessions-db.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SQLite session database — tracks every terminal interaction
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// @ts-ignore — bun:sqlite is a bun built-in
|
|
4
|
+
import { Database } from "bun:sqlite";
|
|
4
5
|
import { existsSync, mkdirSync } from "fs";
|
|
5
6
|
import { homedir } from "os";
|
|
6
7
|
import { join } from "path";
|
|
@@ -9,13 +10,13 @@ import { randomUUID } from "crypto";
|
|
|
9
10
|
const DIR = join(homedir(), ".terminal");
|
|
10
11
|
const DB_PATH = join(DIR, "sessions.db");
|
|
11
12
|
|
|
12
|
-
let db: Database
|
|
13
|
+
let db: Database | null = null;
|
|
13
14
|
|
|
14
|
-
function getDb(): Database
|
|
15
|
+
function getDb(): Database {
|
|
15
16
|
if (db) return db;
|
|
16
17
|
if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
|
|
17
18
|
db = new Database(DB_PATH);
|
|
18
|
-
db.
|
|
19
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
19
20
|
|
|
20
21
|
db.exec(`
|
|
21
22
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -126,7 +127,9 @@ export function logInteraction(sessionId: string, data: {
|
|
|
126
127
|
data.cached ? 1 : 0,
|
|
127
128
|
Date.now()
|
|
128
129
|
);
|
|
129
|
-
|
|
130
|
+
// bun:sqlite — lastInsertRowid is a property on the statement after run()
|
|
131
|
+
const lastId = getDb().prepare("SELECT last_insert_rowid() as id").get() as any;
|
|
132
|
+
return lastId?.id ?? 0;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
export function updateInteraction(id: number, data: {
|