@hasna/terminal 0.5.2 → 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.
@@ -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
- // If within budget or no budget, return structured
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",
@@ -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
- return {
73
- content: [{ type: "text", text: JSON.stringify({
74
- exitCode: result.exitCode, parsed: parsed.data, parser: parsed.parser,
75
- duration: result.duration, tokensSaved: savings.saved, savingsPercent: savings.percent,
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)
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "0.5.2",
3
+ "version": "0.5.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": {
@@ -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
- // If within budget or no budget, return structured
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
- return {
86
- content: [{ type: "text" as const, text: JSON.stringify({
87
- exitCode: result.exitCode, parsed: parsed.data, parser: parsed.parser,
88
- duration: result.duration, tokensSaved: savings.saved, savingsPercent: savings.percent,
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
 
@@ -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
  }