@hasna/terminal 0.5.1 → 0.5.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 +5 -1
- package/dist/compression.js +2 -2
- package/dist/mcp/server.js +10 -7
- package/dist/output-processor.js +26 -0
- package/dist/recipes/storage.js +18 -0
- package/package.json +1 -1
- package/src/App.tsx +5 -1
- package/src/compression.ts +2 -2
- package/src/mcp/server.ts +10 -7
- package/src/output-processor.ts +39 -1
- package/src/recipes/storage.ts +16 -0
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/compression.js
CHANGED
|
@@ -74,8 +74,8 @@ export function compress(command, output, options = {}) {
|
|
|
74
74
|
const json = JSON.stringify(parsed.data, null, format === "summary" ? 0 : 2);
|
|
75
75
|
const savings = tokenSavings(output, parsed.data);
|
|
76
76
|
const compressedTokens = estimateTokens(json);
|
|
77
|
-
//
|
|
78
|
-
if (!maxTokens || compressedTokens <= maxTokens) {
|
|
77
|
+
// ONLY use JSON if it actually saves tokens (never return larger output)
|
|
78
|
+
if (savings.saved > 0 && (!maxTokens || compressedTokens <= maxTokens)) {
|
|
79
79
|
return {
|
|
80
80
|
content: json,
|
|
81
81
|
format: "json",
|
package/dist/mcp/server.js
CHANGED
|
@@ -64,17 +64,20 @@ export function createServer() {
|
|
|
64
64
|
}) }],
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
// JSON mode — structured parsing
|
|
67
|
+
// JSON mode — structured parsing (only if it actually saves tokens)
|
|
68
68
|
if (format === "json") {
|
|
69
69
|
const parsed = parseOutput(command, output);
|
|
70
70
|
if (parsed) {
|
|
71
71
|
const savings = tokenSavings(output, parsed.data);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
if (savings.saved > 0) {
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
75
|
+
exitCode: result.exitCode, parsed: parsed.data, parser: parsed.parser,
|
|
76
|
+
duration: result.duration, tokensSaved: savings.saved, savingsPercent: savings.percent,
|
|
77
|
+
}) }],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// JSON was larger — fall through to compression
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
// Compressed mode (also fallback for json when no parser matches)
|
package/dist/output-processor.js
CHANGED
|
@@ -32,7 +32,11 @@ export async function processOutput(command, output) {
|
|
|
32
32
|
summary: output,
|
|
33
33
|
full: output,
|
|
34
34
|
tokensSaved: 0,
|
|
35
|
+
aiTokensUsed: 0,
|
|
35
36
|
aiProcessed: false,
|
|
37
|
+
aiCostUsd: 0,
|
|
38
|
+
savingsValueUsd: 0,
|
|
39
|
+
netSavingsUsd: 0,
|
|
36
40
|
};
|
|
37
41
|
}
|
|
38
42
|
// Truncate very long output before sending to AI
|
|
@@ -65,12 +69,30 @@ export async function processOutput(command, output) {
|
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
catch { /* not JSON, that's fine */ }
|
|
72
|
+
// Cost calculation
|
|
73
|
+
// AI input: system prompt (~200 tokens) + command + output sent to AI
|
|
74
|
+
const aiInputTokens = estimateTokens(SUMMARIZE_PROMPT) + estimateTokens(toSummarize) + 20;
|
|
75
|
+
const aiOutputTokens = summaryTokens;
|
|
76
|
+
const aiTokensUsed = aiInputTokens + aiOutputTokens;
|
|
77
|
+
// Cerebras qwen-3-235b pricing: $0.60/M input, $1.20/M output
|
|
78
|
+
const aiCostUsd = (aiInputTokens * 0.60 + aiOutputTokens * 1.20) / 1_000_000;
|
|
79
|
+
// Value of tokens saved (at Claude Sonnet $3/M input — what the agent would pay)
|
|
80
|
+
const savingsValueUsd = (saved * 3.0) / 1_000_000;
|
|
81
|
+
const netSavingsUsd = savingsValueUsd - aiCostUsd;
|
|
82
|
+
// Only record savings if net positive (AI cost < token savings value)
|
|
83
|
+
if (netSavingsUsd > 0 && saved > 0) {
|
|
84
|
+
recordSaving("compressed", saved);
|
|
85
|
+
}
|
|
68
86
|
return {
|
|
69
87
|
summary,
|
|
70
88
|
full: output,
|
|
71
89
|
structured,
|
|
72
90
|
tokensSaved: saved,
|
|
91
|
+
aiTokensUsed,
|
|
73
92
|
aiProcessed: true,
|
|
93
|
+
aiCostUsd,
|
|
94
|
+
savingsValueUsd,
|
|
95
|
+
netSavingsUsd,
|
|
74
96
|
};
|
|
75
97
|
}
|
|
76
98
|
catch {
|
|
@@ -82,7 +104,11 @@ export async function processOutput(command, output) {
|
|
|
82
104
|
summary: fallback,
|
|
83
105
|
full: output,
|
|
84
106
|
tokensSaved: Math.max(0, estimateTokens(output) - estimateTokens(fallback)),
|
|
107
|
+
aiTokensUsed: 0,
|
|
85
108
|
aiProcessed: false,
|
|
109
|
+
aiCostUsd: 0,
|
|
110
|
+
savingsValueUsd: 0,
|
|
111
|
+
netSavingsUsd: 0,
|
|
86
112
|
};
|
|
87
113
|
}
|
|
88
114
|
}
|
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/package.json
CHANGED
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/compression.ts
CHANGED
|
@@ -104,8 +104,8 @@ export function compress(command: string, output: string, options: CompressOptio
|
|
|
104
104
|
const savings = tokenSavings(output, parsed.data);
|
|
105
105
|
const compressedTokens = estimateTokens(json);
|
|
106
106
|
|
|
107
|
-
//
|
|
108
|
-
if (!maxTokens || compressedTokens <= maxTokens) {
|
|
107
|
+
// ONLY use JSON if it actually saves tokens (never return larger output)
|
|
108
|
+
if (savings.saved > 0 && (!maxTokens || compressedTokens <= maxTokens)) {
|
|
109
109
|
return {
|
|
110
110
|
content: json,
|
|
111
111
|
format: "json",
|
package/src/mcp/server.ts
CHANGED
|
@@ -77,17 +77,20 @@ export function createServer(): McpServer {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// JSON mode — structured parsing
|
|
80
|
+
// JSON mode — structured parsing (only if it actually saves tokens)
|
|
81
81
|
if (format === "json") {
|
|
82
82
|
const parsed = parseOutput(command, output);
|
|
83
83
|
if (parsed) {
|
|
84
84
|
const savings = tokenSavings(output, parsed.data);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
if (savings.saved > 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
88
|
+
exitCode: result.exitCode, parsed: parsed.data, parser: parsed.parser,
|
|
89
|
+
duration: result.duration, tokensSaved: savings.saved, savingsPercent: savings.percent,
|
|
90
|
+
}) }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// JSON was larger — fall through to compression
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
|
package/src/output-processor.ts
CHANGED
|
@@ -12,10 +12,18 @@ export interface ProcessedOutput {
|
|
|
12
12
|
full: string;
|
|
13
13
|
/** Structured JSON if the AI could extract it */
|
|
14
14
|
structured?: Record<string, unknown>;
|
|
15
|
-
/** How many tokens were saved */
|
|
15
|
+
/** How many tokens were saved (net, after subtracting AI cost) */
|
|
16
16
|
tokensSaved: number;
|
|
17
|
+
/** Tokens used by the AI summarization call */
|
|
18
|
+
aiTokensUsed: number;
|
|
17
19
|
/** Whether AI processing was used (vs passthrough) */
|
|
18
20
|
aiProcessed: boolean;
|
|
21
|
+
/** Cost of the AI call in USD (Cerebras pricing) */
|
|
22
|
+
aiCostUsd: number;
|
|
23
|
+
/** Value of tokens saved in USD (at Claude Sonnet rates) */
|
|
24
|
+
savingsValueUsd: number;
|
|
25
|
+
/** Net ROI: savings minus AI cost */
|
|
26
|
+
netSavingsUsd: number;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
const MIN_LINES_TO_PROCESS = 15;
|
|
@@ -53,7 +61,11 @@ export async function processOutput(
|
|
|
53
61
|
summary: output,
|
|
54
62
|
full: output,
|
|
55
63
|
tokensSaved: 0,
|
|
64
|
+
aiTokensUsed: 0,
|
|
56
65
|
aiProcessed: false,
|
|
66
|
+
aiCostUsd: 0,
|
|
67
|
+
savingsValueUsd: 0,
|
|
68
|
+
netSavingsUsd: 0,
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -94,12 +106,34 @@ export async function processOutput(
|
|
|
94
106
|
}
|
|
95
107
|
} catch { /* not JSON, that's fine */ }
|
|
96
108
|
|
|
109
|
+
// Cost calculation
|
|
110
|
+
// AI input: system prompt (~200 tokens) + command + output sent to AI
|
|
111
|
+
const aiInputTokens = estimateTokens(SUMMARIZE_PROMPT) + estimateTokens(toSummarize) + 20;
|
|
112
|
+
const aiOutputTokens = summaryTokens;
|
|
113
|
+
const aiTokensUsed = aiInputTokens + aiOutputTokens;
|
|
114
|
+
|
|
115
|
+
// Cerebras qwen-3-235b pricing: $0.60/M input, $1.20/M output
|
|
116
|
+
const aiCostUsd = (aiInputTokens * 0.60 + aiOutputTokens * 1.20) / 1_000_000;
|
|
117
|
+
|
|
118
|
+
// Value of tokens saved (at Claude Sonnet $3/M input — what the agent would pay)
|
|
119
|
+
const savingsValueUsd = (saved * 3.0) / 1_000_000;
|
|
120
|
+
const netSavingsUsd = savingsValueUsd - aiCostUsd;
|
|
121
|
+
|
|
122
|
+
// Only record savings if net positive (AI cost < token savings value)
|
|
123
|
+
if (netSavingsUsd > 0 && saved > 0) {
|
|
124
|
+
recordSaving("compressed", saved);
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
return {
|
|
98
128
|
summary,
|
|
99
129
|
full: output,
|
|
100
130
|
structured,
|
|
101
131
|
tokensSaved: saved,
|
|
132
|
+
aiTokensUsed,
|
|
102
133
|
aiProcessed: true,
|
|
134
|
+
aiCostUsd,
|
|
135
|
+
savingsValueUsd,
|
|
136
|
+
netSavingsUsd,
|
|
103
137
|
};
|
|
104
138
|
} catch {
|
|
105
139
|
// AI unavailable — fall back to simple truncation
|
|
@@ -111,7 +145,11 @@ export async function processOutput(
|
|
|
111
145
|
summary: fallback,
|
|
112
146
|
full: output,
|
|
113
147
|
tokensSaved: Math.max(0, estimateTokens(output) - estimateTokens(fallback)),
|
|
148
|
+
aiTokensUsed: 0,
|
|
114
149
|
aiProcessed: false,
|
|
150
|
+
aiCostUsd: 0,
|
|
151
|
+
savingsValueUsd: 0,
|
|
152
|
+
netSavingsUsd: 0,
|
|
115
153
|
};
|
|
116
154
|
}
|
|
117
155
|
}
|
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,
|