@hasna/terminal 4.3.0 → 4.3.1

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.
Files changed (75) hide show
  1. package/package.json +4 -3
  2. package/src/ai.ts +4 -4
  3. package/src/mcp/server.ts +26 -1704
  4. package/src/mcp/tools/batch.ts +106 -0
  5. package/src/mcp/tools/execute.ts +248 -0
  6. package/src/mcp/tools/files.ts +369 -0
  7. package/src/mcp/tools/git.ts +306 -0
  8. package/src/mcp/tools/helpers.ts +92 -0
  9. package/src/mcp/tools/memory.ts +170 -0
  10. package/src/mcp/tools/meta.ts +202 -0
  11. package/src/mcp/tools/process.ts +94 -0
  12. package/src/mcp/tools/project.ts +297 -0
  13. package/src/mcp/tools/search.ts +118 -0
  14. package/src/snapshots.ts +2 -2
  15. package/dist/App.js +0 -404
  16. package/dist/Browse.js +0 -79
  17. package/dist/FuzzyPicker.js +0 -47
  18. package/dist/Onboarding.js +0 -51
  19. package/dist/Spinner.js +0 -12
  20. package/dist/StatusBar.js +0 -49
  21. package/dist/ai.js +0 -315
  22. package/dist/cache.js +0 -42
  23. package/dist/cli.js +0 -778
  24. package/dist/command-rewriter.js +0 -64
  25. package/dist/command-validator.js +0 -86
  26. package/dist/compression.js +0 -91
  27. package/dist/context-hints.js +0 -285
  28. package/dist/diff-cache.js +0 -107
  29. package/dist/discover.js +0 -212
  30. package/dist/economy.js +0 -155
  31. package/dist/expand-store.js +0 -44
  32. package/dist/file-cache.js +0 -72
  33. package/dist/file-index.js +0 -62
  34. package/dist/history.js +0 -62
  35. package/dist/lazy-executor.js +0 -54
  36. package/dist/line-dedup.js +0 -59
  37. package/dist/loop-detector.js +0 -75
  38. package/dist/mcp/install.js +0 -189
  39. package/dist/mcp/server.js +0 -1375
  40. package/dist/noise-filter.js +0 -94
  41. package/dist/output-processor.js +0 -233
  42. package/dist/output-router.js +0 -41
  43. package/dist/output-store.js +0 -111
  44. package/dist/parsers/base.js +0 -2
  45. package/dist/parsers/build.js +0 -64
  46. package/dist/parsers/errors.js +0 -101
  47. package/dist/parsers/files.js +0 -78
  48. package/dist/parsers/git.js +0 -99
  49. package/dist/parsers/index.js +0 -48
  50. package/dist/parsers/tests.js +0 -89
  51. package/dist/providers/anthropic.js +0 -43
  52. package/dist/providers/base.js +0 -4
  53. package/dist/providers/cerebras.js +0 -8
  54. package/dist/providers/groq.js +0 -8
  55. package/dist/providers/index.js +0 -142
  56. package/dist/providers/openai-compat.js +0 -93
  57. package/dist/providers/xai.js +0 -8
  58. package/dist/recipes/model.js +0 -20
  59. package/dist/recipes/storage.js +0 -153
  60. package/dist/search/content-search.js +0 -70
  61. package/dist/search/file-search.js +0 -61
  62. package/dist/search/filters.js +0 -34
  63. package/dist/search/index.js +0 -5
  64. package/dist/search/semantic.js +0 -346
  65. package/dist/session-boot.js +0 -59
  66. package/dist/session-context.js +0 -55
  67. package/dist/sessions-db.js +0 -231
  68. package/dist/smart-display.js +0 -286
  69. package/dist/snapshots.js +0 -51
  70. package/dist/supervisor.js +0 -112
  71. package/dist/test-watchlist.js +0 -131
  72. package/dist/tokens.js +0 -17
  73. package/dist/tool-profiles.js +0 -129
  74. package/dist/tree.js +0 -94
  75. package/dist/usage-cache.js +0 -65
@@ -1,78 +0,0 @@
1
- // Parser for file listing output (ls -la, find, etc.)
2
- const NODE_MODULES_RE = /node_modules/;
3
- const DIST_RE = /\b(dist|build|\.next|__pycache__|coverage|\.git)\b/;
4
- const SOURCE_EXTS = /\.(ts|tsx|js|jsx|py|go|rs|java|rb|sh|c|cpp|h|css|scss|html|vue|svelte|md|json|yaml|yml|toml)$/;
5
- export const lsParser = {
6
- name: "ls",
7
- detect(command, output) {
8
- return /^\s*(ls|ll|la)\b/.test(command) && output.includes(" ");
9
- },
10
- parse(_command, output) {
11
- const lines = output.split("\n").filter(l => l.trim());
12
- const entries = [];
13
- for (const line of lines) {
14
- // ls -la format: drwxr-xr-x 5 user group 160 Mar 10 09:00 dirname
15
- const match = line.match(/^([dlcbps-])([rwxsStT-]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$/);
16
- if (match) {
17
- const typeChar = match[1];
18
- entries.push({
19
- name: match[5],
20
- type: typeChar === "d" ? "dir" : typeChar === "l" ? "symlink" : "file",
21
- size: parseInt(match[3]),
22
- modified: match[4],
23
- permissions: match[1] + match[2],
24
- });
25
- }
26
- else if (line.trim() && !line.startsWith("total ")) {
27
- // Simple ls output — just filenames
28
- entries.push({ name: line.trim(), type: "file" });
29
- }
30
- }
31
- return entries;
32
- },
33
- };
34
- export const findParser = {
35
- name: "find",
36
- detect(command, _output) {
37
- return /^\s*(find|fd)\b/.test(command);
38
- },
39
- parse(_command, output) {
40
- const lines = output.split("\n").filter(l => l.trim());
41
- const source = [];
42
- const other = [];
43
- let nodeModulesCount = 0;
44
- let distCount = 0;
45
- for (const line of lines) {
46
- const path = line.trim();
47
- if (!path)
48
- continue;
49
- if (NODE_MODULES_RE.test(path)) {
50
- nodeModulesCount++;
51
- continue;
52
- }
53
- if (DIST_RE.test(path)) {
54
- distCount++;
55
- continue;
56
- }
57
- const name = path.split("/").pop() ?? path;
58
- const entry = { name: path, type: SOURCE_EXTS.test(name) ? "file" : "other" };
59
- if (SOURCE_EXTS.test(name)) {
60
- source.push(entry);
61
- }
62
- else {
63
- other.push(entry);
64
- }
65
- }
66
- const filtered = [];
67
- if (nodeModulesCount > 0)
68
- filtered.push({ count: nodeModulesCount, reason: "node_modules" });
69
- if (distCount > 0)
70
- filtered.push({ count: distCount, reason: "dist/build" });
71
- return {
72
- total: lines.length,
73
- source,
74
- other,
75
- filtered,
76
- };
77
- },
78
- };
@@ -1,99 +0,0 @@
1
- // Parsers for git output (log, status, diff)
2
- export const gitLogParser = {
3
- name: "git-log",
4
- detect(command, _output) {
5
- return /\bgit\s+log\b/.test(command);
6
- },
7
- parse(_command, output) {
8
- const entries = [];
9
- const lines = output.split("\n");
10
- // Detect oneline format: "abc1234 commit message"
11
- const firstLine = lines[0]?.trim() ?? "";
12
- const isOneline = /^[a-f0-9]{7,12}\s+/.test(firstLine) && !firstLine.startsWith("commit ");
13
- if (isOneline) {
14
- for (const line of lines) {
15
- const match = line.trim().match(/^([a-f0-9]{7,12})\s+(.+)$/);
16
- if (match) {
17
- entries.push({ hash: match[1], author: "", date: "", message: match[2] });
18
- }
19
- }
20
- return entries;
21
- }
22
- // Verbose format
23
- let hash = "", author = "", date = "", message = [];
24
- for (const line of lines) {
25
- const commitMatch = line.match(/^commit\s+([a-f0-9]+)/);
26
- if (commitMatch) {
27
- if (hash) {
28
- entries.push({ hash: hash.slice(0, 8), author, date, message: message.join(" ").trim() });
29
- }
30
- hash = commitMatch[1];
31
- author = "";
32
- date = "";
33
- message = [];
34
- continue;
35
- }
36
- const authorMatch = line.match(/^Author:\s+(.+)/);
37
- if (authorMatch) {
38
- author = authorMatch[1];
39
- continue;
40
- }
41
- const dateMatch = line.match(/^Date:\s+(.+)/);
42
- if (dateMatch) {
43
- date = dateMatch[1].trim();
44
- continue;
45
- }
46
- if (line.startsWith(" ")) {
47
- message.push(line.trim());
48
- }
49
- }
50
- if (hash) {
51
- entries.push({ hash: hash.slice(0, 8), author, date, message: message.join(" ").trim() });
52
- }
53
- return entries;
54
- },
55
- };
56
- export const gitStatusParser = {
57
- name: "git-status",
58
- detect(command, _output) {
59
- return /\bgit\s+status\b/.test(command);
60
- },
61
- parse(_command, output) {
62
- const lines = output.split("\n");
63
- let branch = "";
64
- const staged = [];
65
- const unstaged = [];
66
- const untracked = [];
67
- const branchMatch = output.match(/On branch\s+(\S+)/);
68
- if (branchMatch)
69
- branch = branchMatch[1];
70
- let section = "";
71
- for (const line of lines) {
72
- if (line.includes("Changes to be committed")) {
73
- section = "staged";
74
- continue;
75
- }
76
- if (line.includes("Changes not staged")) {
77
- section = "unstaged";
78
- continue;
79
- }
80
- if (line.includes("Untracked files")) {
81
- section = "untracked";
82
- continue;
83
- }
84
- const fileMatch = line.match(/^\s+(?:new file|modified|deleted|renamed):\s+(.+)/);
85
- if (fileMatch) {
86
- if (section === "staged")
87
- staged.push(fileMatch[1].trim());
88
- else if (section === "unstaged")
89
- unstaged.push(fileMatch[1].trim());
90
- continue;
91
- }
92
- // Untracked files are just indented filenames
93
- if (section === "untracked" && line.match(/^\s+\S/) && !line.includes("(use ")) {
94
- untracked.push(line.trim());
95
- }
96
- }
97
- return { branch, staged, unstaged, untracked };
98
- },
99
- };
@@ -1,48 +0,0 @@
1
- // Output parser registry — auto-detect command output type and parse to structured JSON
2
- import { lsParser, findParser } from "./files.js";
3
- import { testParser } from "./tests.js";
4
- import { gitLogParser, gitStatusParser } from "./git.js";
5
- import { buildParser, npmInstallParser } from "./build.js";
6
- import { errorParser } from "./errors.js";
7
- // Ordered by specificity — more specific parsers first
8
- const parsers = [
9
- npmInstallParser,
10
- testParser,
11
- gitLogParser,
12
- gitStatusParser,
13
- buildParser,
14
- findParser,
15
- lsParser,
16
- errorParser, // fallback for error detection
17
- ];
18
- /** Try to parse command output with the best matching parser */
19
- export function parseOutput(command, output) {
20
- for (const parser of parsers) {
21
- if (parser.detect(command, output)) {
22
- try {
23
- const data = parser.parse(command, output);
24
- return { parser: parser.name, data, raw: output };
25
- }
26
- catch {
27
- continue;
28
- }
29
- }
30
- }
31
- return null;
32
- }
33
- /** Get all parsers that match (for debugging/info) */
34
- export function detectParsers(command, output) {
35
- return parsers.filter(p => p.detect(command, output)).map(p => p.name);
36
- }
37
- /** Estimate token count for a string (rough: ~4 chars per token) */
38
- export function estimateTokens(text) {
39
- return Math.ceil(text.length / 4);
40
- }
41
- /** Calculate token savings between raw output and parsed JSON */
42
- export function tokenSavings(raw, parsed) {
43
- const rawTokens = estimateTokens(raw);
44
- const parsedTokens = estimateTokens(JSON.stringify(parsed));
45
- const saved = Math.max(0, rawTokens - parsedTokens);
46
- const percent = rawTokens > 0 ? Math.round((saved / rawTokens) * 100) : 0;
47
- return { rawTokens, parsedTokens, saved, percent };
48
- }
@@ -1,89 +0,0 @@
1
- // Parser for test runner output (jest, vitest, bun test, pytest, go test)
2
- export const testParser = {
3
- name: "test",
4
- detect(command, output) {
5
- if (/\b(jest|vitest|bun\s+test|pytest|go\s+test|mocha|ava|tap)\b/.test(command))
6
- return true;
7
- if (/\b(npm|bun|pnpm|yarn)\s+(run\s+)?test\b/.test(command))
8
- return true;
9
- // Detect by output patterns
10
- return /Tests:\s+\d+/.test(output) || /\d+\s+(passing|passed|failed)/.test(output) || /PASS|FAIL/.test(output);
11
- },
12
- parse(_command, output) {
13
- const failures = [];
14
- let passed = 0, failed = 0, skipped = 0, duration;
15
- // Jest/Vitest style: Tests: 5 passed, 2 failed, 7 total
16
- const jestMatch = output.match(/Tests:\s+(?:(\d+)\s+passed)?[,\s]*(?:(\d+)\s+failed)?[,\s]*(?:(\d+)\s+skipped)?[,\s]*(\d+)\s+total/);
17
- if (jestMatch) {
18
- passed = parseInt(jestMatch[1] ?? "0");
19
- failed = parseInt(jestMatch[2] ?? "0");
20
- skipped = parseInt(jestMatch[3] ?? "0");
21
- }
22
- // Bun test style: 5 pass, 2 fail
23
- const bunMatch = output.match(/(\d+)\s+pass.*?(\d+)\s+fail/);
24
- if (!jestMatch && bunMatch) {
25
- passed = parseInt(bunMatch[1]);
26
- failed = parseInt(bunMatch[2]);
27
- }
28
- // Pytest style: 5 passed, 2 failed
29
- const pytestMatch = output.match(/(\d+)\s+passed(?:.*?(\d+)\s+failed)?/);
30
- if (!jestMatch && !bunMatch && pytestMatch) {
31
- passed = parseInt(pytestMatch[1]);
32
- failed = parseInt(pytestMatch[2] ?? "0");
33
- }
34
- // Go test: ok/FAIL + count
35
- const goPassMatch = output.match(/ok\s+\S+\s+([\d.]+s)/);
36
- const goFailMatch = output.match(/FAIL\s+\S+/);
37
- if (!jestMatch && !bunMatch && !pytestMatch && (goPassMatch || goFailMatch)) {
38
- const passLines = (output.match(/--- PASS/g) || []).length;
39
- const failLines = (output.match(/--- FAIL/g) || []).length;
40
- passed = passLines;
41
- failed = failLines;
42
- if (goPassMatch)
43
- duration = goPassMatch[1];
44
- }
45
- // Duration
46
- const timeMatch = output.match(/Time:\s+([\d.]+\s*(?:s|ms|m))/i) || output.match(/in\s+([\d.]+\s*(?:s|ms|m))/i);
47
- if (timeMatch)
48
- duration = timeMatch[1];
49
- // Extract failure details: lines starting with FAIL or ✗ or ×
50
- const lines = output.split("\n");
51
- let capturingFailure = false;
52
- let currentTest = "";
53
- let currentError = [];
54
- for (const line of lines) {
55
- const failMatch = line.match(/(?:FAIL|✗|×|✕)\s+(.+)/);
56
- if (failMatch) {
57
- if (capturingFailure && currentTest) {
58
- failures.push({ test: currentTest, error: currentError.join("\n").trim() });
59
- }
60
- currentTest = failMatch[1].trim();
61
- currentError = [];
62
- capturingFailure = true;
63
- continue;
64
- }
65
- if (capturingFailure) {
66
- if (line.match(/^(PASS|✓|✔|FAIL|✗|×|✕)\s/) || line.match(/^Tests:|^\d+ pass/)) {
67
- failures.push({ test: currentTest, error: currentError.join("\n").trim() });
68
- capturingFailure = false;
69
- currentTest = "";
70
- currentError = [];
71
- }
72
- else {
73
- currentError.push(line);
74
- }
75
- }
76
- }
77
- if (capturingFailure && currentTest) {
78
- failures.push({ test: currentTest, error: currentError.join("\n").trim() });
79
- }
80
- return {
81
- passed,
82
- failed,
83
- skipped,
84
- total: passed + failed + skipped,
85
- duration,
86
- failures,
87
- };
88
- },
89
- };
@@ -1,43 +0,0 @@
1
- import Anthropic from "@anthropic-ai/sdk";
2
- export class AnthropicProvider {
3
- name = "anthropic";
4
- client;
5
- constructor() {
6
- this.client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
7
- }
8
- isAvailable() {
9
- return !!process.env.ANTHROPIC_API_KEY;
10
- }
11
- async complete(prompt, options) {
12
- const message = await this.client.messages.create({
13
- model: options.model ?? "claude-haiku-4-5-20251001",
14
- max_tokens: options.maxTokens ?? 256,
15
- temperature: options.temperature ?? 0,
16
- ...(options.stop ? { stop_sequences: options.stop } : {}),
17
- system: [{ type: "text", text: options.system, cache_control: { type: "ephemeral" } }],
18
- messages: [{ role: "user", content: prompt }],
19
- });
20
- const block = message.content[0];
21
- if (block.type !== "text")
22
- throw new Error("Unexpected response type");
23
- return block.text.trim();
24
- }
25
- async stream(prompt, options, callbacks) {
26
- let result = "";
27
- const stream = await this.client.messages.stream({
28
- model: options.model ?? "claude-haiku-4-5-20251001",
29
- max_tokens: options.maxTokens ?? 256,
30
- temperature: options.temperature ?? 0,
31
- ...(options.stop ? { stop_sequences: options.stop } : {}),
32
- system: [{ type: "text", text: options.system, cache_control: { type: "ephemeral" } }],
33
- messages: [{ role: "user", content: prompt }],
34
- });
35
- for await (const chunk of stream) {
36
- if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
37
- result += chunk.delta.text;
38
- callbacks.onToken(result.trim());
39
- }
40
- }
41
- return result.trim();
42
- }
43
- }
@@ -1,4 +0,0 @@
1
- // Provider interface for LLM backends (Anthropic, Cerebras, etc.)
2
- export const DEFAULT_PROVIDER_CONFIG = {
3
- provider: "auto",
4
- };
@@ -1,8 +0,0 @@
1
- // Cerebras provider — fast inference on Qwen/Llama models
2
- import { OpenAICompatibleProvider } from "./openai-compat.js";
3
- export class CerebrasProvider extends OpenAICompatibleProvider {
4
- name = "cerebras";
5
- baseUrl = "https://api.cerebras.ai/v1";
6
- defaultModel = "qwen-3-235b-a22b-instruct-2507";
7
- apiKeyEnvVar = "CEREBRAS_API_KEY";
8
- }
@@ -1,8 +0,0 @@
1
- // Groq provider — ultra-fast inference
2
- import { OpenAICompatibleProvider } from "./openai-compat.js";
3
- export class GroqProvider extends OpenAICompatibleProvider {
4
- name = "groq";
5
- baseUrl = "https://api.groq.com/openai/v1";
6
- defaultModel = "openai/gpt-oss-120b";
7
- apiKeyEnvVar = "GROQ_API_KEY";
8
- }
@@ -1,142 +0,0 @@
1
- // Provider auto-detection and management — with fallback on failure
2
- import { DEFAULT_PROVIDER_CONFIG } from "./base.js";
3
- import { AnthropicProvider } from "./anthropic.js";
4
- import { CerebrasProvider } from "./cerebras.js";
5
- import { GroqProvider } from "./groq.js";
6
- import { XaiProvider } from "./xai.js";
7
- export { DEFAULT_PROVIDER_CONFIG } from "./base.js";
8
- let _provider = null;
9
- let _outputProvider = null;
10
- let _failedProviders = new Set();
11
- /** Get the active LLM provider. Auto-detects based on available API keys. */
12
- export function getProvider(config) {
13
- if (_provider && !_failedProviders.has(_provider.name))
14
- return _provider;
15
- const cfg = config ?? DEFAULT_PROVIDER_CONFIG;
16
- _provider = resolveProvider(cfg);
17
- return _provider;
18
- }
19
- /** Reset the cached provider (useful when config changes). */
20
- export function resetProvider() {
21
- _provider = null;
22
- _outputProvider = null;
23
- _failedProviders.clear();
24
- }
25
- /**
26
- * Get the provider optimized for output summarization.
27
- * Priority: Groq (fastest, 234ms avg) > Cerebras > xAI > Anthropic.
28
- * Falls back to the main provider if Groq is unavailable.
29
- */
30
- export function getOutputProvider() {
31
- if (_outputProvider)
32
- return _outputProvider;
33
- // Prefer Groq for output processing (fastest + best compression in evals)
34
- const groq = new GroqProvider();
35
- if (groq.isAvailable()) {
36
- _outputProvider = groq;
37
- return groq;
38
- }
39
- // Fall back to main provider
40
- _outputProvider = getProvider();
41
- return _outputProvider;
42
- }
43
- /** Get a fallback-wrapped provider that tries alternatives on failure */
44
- export function getProviderWithFallback(config) {
45
- const primary = getProvider(config);
46
- return new FallbackProvider(primary);
47
- }
48
- function resolveProvider(config) {
49
- if (config.provider !== "auto") {
50
- const providers = {
51
- cerebras: () => new CerebrasProvider(),
52
- anthropic: () => new AnthropicProvider(),
53
- groq: () => new GroqProvider(),
54
- xai: () => new XaiProvider(),
55
- };
56
- const factory = providers[config.provider];
57
- if (factory) {
58
- const p = factory();
59
- if (!p.isAvailable())
60
- throw new Error(`${config.provider.toUpperCase()}_API_KEY not set`);
61
- return p;
62
- }
63
- }
64
- // auto: prefer Cerebras, then xAI, then Groq, then Anthropic — skip failed
65
- const candidates = [
66
- new CerebrasProvider(),
67
- new XaiProvider(),
68
- new GroqProvider(),
69
- new AnthropicProvider(),
70
- ];
71
- for (const p of candidates) {
72
- if (p.isAvailable() && !_failedProviders.has(p.name))
73
- return p;
74
- }
75
- // If all failed, clear failures and try again
76
- if (_failedProviders.size > 0) {
77
- _failedProviders.clear();
78
- for (const p of candidates) {
79
- if (p.isAvailable())
80
- return p;
81
- }
82
- }
83
- throw new Error("No API key found. Set one of:\n" +
84
- " export CEREBRAS_API_KEY=your-key (free, open-source)\n" +
85
- " export GROQ_API_KEY=your-key (free, fast)\n" +
86
- " export XAI_API_KEY=your-key (Grok, code-optimized)\n" +
87
- " export ANTHROPIC_API_KEY=your-key (Claude)");
88
- }
89
- /** Provider wrapper that falls back to alternatives on API errors */
90
- class FallbackProvider {
91
- name;
92
- primary;
93
- constructor(primary) {
94
- this.primary = primary;
95
- this.name = primary.name;
96
- }
97
- isAvailable() {
98
- return this.primary.isAvailable();
99
- }
100
- async complete(prompt, options) {
101
- try {
102
- return await this.primary.complete(prompt, options);
103
- }
104
- catch (err) {
105
- const fallback = this.getFallback();
106
- if (fallback)
107
- return fallback.complete(prompt, options);
108
- throw err;
109
- }
110
- }
111
- async stream(prompt, options, callbacks) {
112
- try {
113
- return await this.primary.stream(prompt, options, callbacks);
114
- }
115
- catch (err) {
116
- const fallback = this.getFallback();
117
- if (fallback)
118
- return fallback.complete(prompt, options); // fallback doesn't stream
119
- throw err;
120
- }
121
- }
122
- getFallback() {
123
- _failedProviders.add(this.primary.name);
124
- _provider = null; // force re-resolve
125
- try {
126
- const next = getProvider();
127
- if (next.name !== this.primary.name)
128
- return next;
129
- }
130
- catch { }
131
- return null;
132
- }
133
- }
134
- /** List available providers (for onboarding UI). */
135
- export function availableProviders() {
136
- return [
137
- { name: "cerebras", available: new CerebrasProvider().isAvailable() },
138
- { name: "groq", available: new GroqProvider().isAvailable() },
139
- { name: "xai", available: new XaiProvider().isAvailable() },
140
- { name: "anthropic", available: new AnthropicProvider().isAvailable() },
141
- ];
142
- }
@@ -1,93 +0,0 @@
1
- // Shared base class for OpenAI-compatible providers (Cerebras, Groq, xAI)
2
- // Eliminates ~200 lines of duplicated streaming SSE parsing
3
- export class OpenAICompatibleProvider {
4
- get apiKey() {
5
- return process.env[this.apiKeyEnvVar] ?? "";
6
- }
7
- isAvailable() {
8
- return !!process.env[this.apiKeyEnvVar];
9
- }
10
- async complete(prompt, options) {
11
- const res = await fetch(`${this.baseUrl}/chat/completions`, {
12
- method: "POST",
13
- headers: {
14
- "Content-Type": "application/json",
15
- Authorization: `Bearer ${this.apiKey}`,
16
- },
17
- body: JSON.stringify({
18
- model: options.model ?? this.defaultModel,
19
- max_tokens: options.maxTokens ?? 256,
20
- temperature: options.temperature ?? 0,
21
- ...(options.stop ? { stop: options.stop } : {}),
22
- messages: [
23
- { role: "system", content: options.system },
24
- { role: "user", content: prompt },
25
- ],
26
- }),
27
- });
28
- if (!res.ok) {
29
- const text = await res.text();
30
- throw new Error(`${this.name} API error ${res.status}: ${text}`);
31
- }
32
- const json = (await res.json());
33
- return (json.choices?.[0]?.message?.content ?? "").trim();
34
- }
35
- async stream(prompt, options, callbacks) {
36
- const res = await fetch(`${this.baseUrl}/chat/completions`, {
37
- method: "POST",
38
- headers: {
39
- "Content-Type": "application/json",
40
- Authorization: `Bearer ${this.apiKey}`,
41
- },
42
- body: JSON.stringify({
43
- model: options.model ?? this.defaultModel,
44
- max_tokens: options.maxTokens ?? 256,
45
- temperature: options.temperature ?? 0,
46
- stream: true,
47
- ...(options.stop ? { stop: options.stop } : {}),
48
- messages: [
49
- { role: "system", content: options.system },
50
- { role: "user", content: prompt },
51
- ],
52
- }),
53
- });
54
- if (!res.ok) {
55
- const text = await res.text();
56
- throw new Error(`${this.name} API error ${res.status}: ${text}`);
57
- }
58
- let result = "";
59
- const reader = res.body?.getReader();
60
- if (!reader)
61
- throw new Error("No response body");
62
- const decoder = new TextDecoder();
63
- let buffer = "";
64
- while (true) {
65
- const { done, value } = await reader.read();
66
- if (done)
67
- break;
68
- buffer += decoder.decode(value, { stream: true });
69
- const lines = buffer.split("\n");
70
- buffer = lines.pop() ?? "";
71
- for (const line of lines) {
72
- const trimmed = line.trim();
73
- if (!trimmed.startsWith("data: "))
74
- continue;
75
- const data = trimmed.slice(6);
76
- if (data === "[DONE]")
77
- break;
78
- try {
79
- const parsed = JSON.parse(data);
80
- const delta = parsed.choices?.[0]?.delta?.content;
81
- if (delta) {
82
- result += delta;
83
- callbacks.onToken(result.trim());
84
- }
85
- }
86
- catch {
87
- // skip malformed chunks
88
- }
89
- }
90
- }
91
- return result.trim();
92
- }
93
- }
@@ -1,8 +0,0 @@
1
- // xAI/Grok provider — code-optimized models
2
- import { OpenAICompatibleProvider } from "./openai-compat.js";
3
- export class XaiProvider extends OpenAICompatibleProvider {
4
- name = "xai";
5
- baseUrl = "https://api.x.ai/v1";
6
- defaultModel = "grok-code-fast-1";
7
- apiKeyEnvVar = "XAI_API_KEY";
8
- }
@@ -1,20 +0,0 @@
1
- // Recipes data model — reusable command templates with collections and projects
2
- /** Generate a short random ID */
3
- export function genId() {
4
- return Math.random().toString(36).slice(2, 10);
5
- }
6
- /** Substitute variables in a command template */
7
- export function substituteVariables(command, vars) {
8
- let result = command;
9
- for (const [name, value] of Object.entries(vars)) {
10
- result = result.replace(new RegExp(`\\{${name}\\}`, "g"), value);
11
- }
12
- return result;
13
- }
14
- /** Extract variable placeholders from a command */
15
- export function extractVariables(command) {
16
- const matches = command.match(/\{(\w+)\}/g);
17
- if (!matches)
18
- return [];
19
- return [...new Set(matches.map(m => m.slice(1, -1)))];
20
- }