@hasna/terminal 2.0.5 → 2.2.0
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/cli.js +23 -9
- package/package.json +1 -1
- package/src/ai.ts +46 -113
- package/src/cli.tsx +22 -9
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +202 -0
- package/src/output-processor.ts +7 -18
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
package/dist/cli.js
CHANGED
|
@@ -41,7 +41,9 @@ MCP TOOLS (20+):
|
|
|
41
41
|
snapshot, token_stats, session_history
|
|
42
42
|
|
|
43
43
|
ENVIRONMENT:
|
|
44
|
-
|
|
44
|
+
XAI_API_KEY xAI API key (Grok, code-optimized — default)
|
|
45
|
+
CEREBRAS_API_KEY Cerebras API key (free, open-source)
|
|
46
|
+
GROQ_API_KEY Groq API key (free, ultra-fast inference)
|
|
45
47
|
ANTHROPIC_API_KEY Anthropic API key (Claude models)
|
|
46
48
|
`);
|
|
47
49
|
process.exit(0);
|
|
@@ -397,7 +399,7 @@ else if (args[0] === "history") {
|
|
|
397
399
|
// ── Explain command ─────────────────────────────────────────────────────────
|
|
398
400
|
else if (args[0] === "explain" && args[1]) {
|
|
399
401
|
const command = args.slice(1).join(" ");
|
|
400
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
|
|
402
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY) {
|
|
401
403
|
console.error("explain requires an API key");
|
|
402
404
|
process.exit(1);
|
|
403
405
|
}
|
|
@@ -420,7 +422,7 @@ else if (args[0] === "project" && args[1] === "init") {
|
|
|
420
422
|
else if (args.length > 0) {
|
|
421
423
|
// Everything that doesn't match a subcommand is treated as natural language
|
|
422
424
|
const prompt = args.join(" ");
|
|
423
|
-
const offlineMode = !process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY;
|
|
425
|
+
const offlineMode = !process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY;
|
|
424
426
|
const { translateToCommand, checkPermissions, isIrreversible } = await import("./ai.js");
|
|
425
427
|
const { execSync } = await import("child_process");
|
|
426
428
|
const { compress, stripAnsi } = await import("./compression.js");
|
|
@@ -477,9 +479,10 @@ else if (args.length > 0) {
|
|
|
477
479
|
catch { }
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
|
-
//
|
|
482
|
+
// Show the block reason clearly
|
|
481
483
|
if (e.message?.startsWith("BLOCKED:")) {
|
|
482
|
-
console.log(
|
|
484
|
+
console.log(`⚠ ${e.message}`);
|
|
485
|
+
console.log(` This is a READ-ONLY terminal. Run directly in your shell if you're sure.`);
|
|
483
486
|
}
|
|
484
487
|
else {
|
|
485
488
|
console.error(e.message);
|
|
@@ -525,13 +528,22 @@ else if (args.length > 0) {
|
|
|
525
528
|
// Auto-retry: re-translate with simpler constraints
|
|
526
529
|
console.error(`[open-terminal] invalid command detected: ${validation.issues.join(", ")}`);
|
|
527
530
|
try {
|
|
528
|
-
const retryCommand = await translateToCommand(`${prompt} (
|
|
531
|
+
const retryCommand = await translateToCommand(`${prompt} (Previous command had issues: ${validation.issues.join(", ")}. Fix those specific issues. Keep the approach but correct the errors.)`, perms, []);
|
|
529
532
|
if (retryCommand && retryCommand !== command) {
|
|
530
533
|
const retryValidation = validateCommand(retryCommand, process.cwd());
|
|
531
534
|
if (retryValidation.valid || retryValidation.issues.length < validation.issues.length) {
|
|
532
535
|
command = retryCommand;
|
|
533
536
|
console.error(`[open-terminal] retried: $ ${command}`);
|
|
534
537
|
}
|
|
538
|
+
else {
|
|
539
|
+
// Retry also invalid — use the simpler of the two
|
|
540
|
+
const retryPipes = (retryCommand.match(/\|/g) || []).length;
|
|
541
|
+
const origPipes = (command.match(/\|/g) || []).length;
|
|
542
|
+
if (retryPipes < origPipes) {
|
|
543
|
+
command = retryCommand;
|
|
544
|
+
console.error(`[open-terminal] retried (simpler): $ ${command}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
535
547
|
}
|
|
536
548
|
}
|
|
537
549
|
catch { }
|
|
@@ -652,11 +664,13 @@ else if (args.length > 0) {
|
|
|
652
664
|
}
|
|
653
665
|
// ── TUI mode (no args) ──────────────────────────────────────────────────────
|
|
654
666
|
else {
|
|
655
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
|
|
667
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY) {
|
|
656
668
|
console.error("terminal: No API key found.");
|
|
657
669
|
console.error("Set one of:");
|
|
658
|
-
console.error(" export
|
|
659
|
-
console.error(" export
|
|
670
|
+
console.error(" export XAI_API_KEY=your_key (Grok, code-optimized — default)");
|
|
671
|
+
console.error(" export CEREBRAS_API_KEY=your_key (free, open-source)");
|
|
672
|
+
console.error(" export GROQ_API_KEY=your_key (free, ultra-fast)");
|
|
673
|
+
console.error(" export ANTHROPIC_API_KEY=your_key (Claude)");
|
|
660
674
|
process.exit(1);
|
|
661
675
|
}
|
|
662
676
|
const App = (await import("./App.js")).default;
|
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { cacheGet, cacheSet } from "./cache.js";
|
|
|
3
3
|
import { getProvider } from "./providers/index.js";
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
|
+
import { discoverProjectHints, discoverSafetyHints, formatHints } from "./context-hints.js";
|
|
6
7
|
|
|
7
8
|
// ── model routing ─────────────────────────────────────────────────────────────
|
|
8
9
|
// Simple queries → fast model. Complex/ambiguous → smart model.
|
|
@@ -30,6 +31,22 @@ function pickModel(nl: string): { fast: string; smart: string; pick: "fast" | "s
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
if (provider.name === "groq") {
|
|
35
|
+
return {
|
|
36
|
+
fast: "openai/gpt-oss-120b",
|
|
37
|
+
smart: "moonshotai/kimi-k2-instruct",
|
|
38
|
+
pick: isComplex ? "smart" : "fast",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (provider.name === "xai") {
|
|
43
|
+
return {
|
|
44
|
+
fast: "grok-code-fast-1",
|
|
45
|
+
smart: "grok-4-fast-non-reasoning",
|
|
46
|
+
pick: isComplex ? "smart" : "fast",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
// Cerebras — qwen for everything (llama3.1-8b too unreliable)
|
|
34
51
|
return {
|
|
35
52
|
fast: "qwen-3-235b-a22b-instruct-2507",
|
|
@@ -107,118 +124,11 @@ export interface SessionEntry {
|
|
|
107
124
|
error?: boolean;
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
// ── project context
|
|
127
|
+
// ── project context (powered by context-hints) ──────────────────────────────
|
|
111
128
|
|
|
112
129
|
function detectProjectContext(): string {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Node.js / TypeScript
|
|
117
|
-
const pkgPath = join(cwd, "package.json");
|
|
118
|
-
if (existsSync(pkgPath)) {
|
|
119
|
-
try {
|
|
120
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
121
|
-
parts.push(`Project: ${pkg.name}@${pkg.version} (Node.js/TypeScript)`);
|
|
122
|
-
parts.push(`npm package: ${pkg.name} (use this name for npm view, npm info, etc.)`);
|
|
123
|
-
if (pkg.scripts) {
|
|
124
|
-
const scripts = Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).slice(0, 8);
|
|
125
|
-
parts.push(`Available scripts: ${scripts.join(", ")}`);
|
|
126
|
-
}
|
|
127
|
-
if (pkg.dependencies) parts.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
128
|
-
parts.push(`Use npm/bun/pnpm commands, NOT maven/gradle/cargo.`);
|
|
129
|
-
} catch {}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Python
|
|
133
|
-
if (existsSync(join(cwd, "pyproject.toml"))) {
|
|
134
|
-
try {
|
|
135
|
-
const pyproject = readFileSync(join(cwd, "pyproject.toml"), "utf8");
|
|
136
|
-
const nameMatch = pyproject.match(/name\s*=\s*"([^"]+)"/);
|
|
137
|
-
const versionMatch = pyproject.match(/version\s*=\s*"([^"]+)"/);
|
|
138
|
-
parts.push(`Project: ${nameMatch?.[1] ?? "Python"}${versionMatch ? `@${versionMatch[1]}` : ""} (Python)`);
|
|
139
|
-
} catch { parts.push("Project: Python (pyproject.toml found)"); }
|
|
140
|
-
parts.push("Use pip/python/pytest commands. Test: pytest. Build: python -m build.");
|
|
141
|
-
} else if (existsSync(join(cwd, "requirements.txt"))) {
|
|
142
|
-
parts.push("Project: Python (requirements.txt). Use pip/python/pytest commands.");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Go
|
|
146
|
-
if (existsSync(join(cwd, "go.mod"))) {
|
|
147
|
-
try {
|
|
148
|
-
const gomod = readFileSync(join(cwd, "go.mod"), "utf8");
|
|
149
|
-
const moduleMatch = gomod.match(/module\s+(\S+)/);
|
|
150
|
-
parts.push(`Project: ${moduleMatch?.[1] ?? "Go"} (Go module)`);
|
|
151
|
-
} catch { parts.push("Project: Go (go.mod found)"); }
|
|
152
|
-
parts.push("Use go build/test/run. Test: go test ./... Build: go build.");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Rust
|
|
156
|
-
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
157
|
-
try {
|
|
158
|
-
const cargo = readFileSync(join(cwd, "Cargo.toml"), "utf8");
|
|
159
|
-
const nameMatch = cargo.match(/name\s*=\s*"([^"]+)"/);
|
|
160
|
-
const versionMatch = cargo.match(/version\s*=\s*"([^"]+)"/);
|
|
161
|
-
parts.push(`Project: ${nameMatch?.[1] ?? "Rust"}${versionMatch ? `@${versionMatch[1]}` : ""} (Rust/Cargo)`);
|
|
162
|
-
} catch { parts.push("Project: Rust (Cargo.toml found)"); }
|
|
163
|
-
parts.push("Use cargo build/test/run. Test: cargo test. Build: cargo build --release.");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Java
|
|
167
|
-
if (existsSync(join(cwd, "pom.xml"))) {
|
|
168
|
-
parts.push("Project: Java/Maven. Use mvn commands. Test: mvn test. Build: mvn package.");
|
|
169
|
-
}
|
|
170
|
-
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
|
|
171
|
-
parts.push("Project: Java/Gradle. Use gradle commands. Test: gradle test. Build: gradle build.");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Docker
|
|
175
|
-
if (existsSync(join(cwd, "Dockerfile")) || existsSync(join(cwd, "docker-compose.yml")) || existsSync(join(cwd, "docker-compose.yaml"))) {
|
|
176
|
-
parts.push("Docker: Dockerfile/docker-compose present. Container commands available.");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Makefile
|
|
180
|
-
if (existsSync(join(cwd, "Makefile"))) {
|
|
181
|
-
try {
|
|
182
|
-
const { execSync: execS } = require("child_process");
|
|
183
|
-
const targets = execS("grep -E '^[a-zA-Z_-]+:' Makefile | head -10 | cut -d: -f1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
184
|
-
if (targets) parts.push(`Makefile targets: ${targets.split("\n").join(", ")}`);
|
|
185
|
-
} catch {}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Directory structure — so AI knows actual paths (not guessed ones)
|
|
189
|
-
try {
|
|
190
|
-
const { execSync } = require("child_process");
|
|
191
|
-
// Top-level dirs
|
|
192
|
-
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 2000 }).trim();
|
|
193
|
-
parts.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
194
|
-
|
|
195
|
-
// Detect monorepo (packages/ or workspaces in package.json)
|
|
196
|
-
const isMonorepo = existsSync(join(cwd, "packages")) || existsSync(join(cwd, "apps"));
|
|
197
|
-
if (isMonorepo) {
|
|
198
|
-
const pkgDirs = execSync(
|
|
199
|
-
`ls -d packages/*/src 2>/dev/null || ls -d apps/*/src 2>/dev/null || echo ""`,
|
|
200
|
-
{ cwd, encoding: "utf8", timeout: 2000 }
|
|
201
|
-
).trim();
|
|
202
|
-
if (pkgDirs) {
|
|
203
|
-
parts.push(`MONOREPO: Source is in packages/*/src/, NOT src/. Search packages/ not src/.`);
|
|
204
|
-
parts.push(`Package sources:\n${pkgDirs}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/ structure — include FILES so AI knows exact filenames + extensions
|
|
209
|
-
for (const srcDir of isMonorepo ? ["packages"] : ["src", "lib", "app"]) {
|
|
210
|
-
if (existsSync(join(cwd, srcDir))) {
|
|
211
|
-
const tree = execSync(
|
|
212
|
-
`find ${srcDir} -maxdepth ${isMonorepo ? 4 : 3} -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | sort | head -80`,
|
|
213
|
-
{ cwd, encoding: "utf8", timeout: 3000 }
|
|
214
|
-
).trim();
|
|
215
|
-
if (tree) parts.push(`Files in ${srcDir}/:\n${tree}`);
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch { /* timeout or no exec — skip */ }
|
|
220
|
-
|
|
221
|
-
return parts.length > 0 ? `\n\nPROJECT CONTEXT:\n${parts.join("\n")}` : "";
|
|
130
|
+
const hints = discoverProjectHints(process.cwd());
|
|
131
|
+
return hints.length > 0 ? `\n\n${formatHints(hints)}` : "";
|
|
222
132
|
}
|
|
223
133
|
|
|
224
134
|
// ── system prompt ─────────────────────────────────────────────────────────────
|
|
@@ -254,10 +164,24 @@ function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[]):
|
|
|
254
164
|
|
|
255
165
|
const projectContext = detectProjectContext();
|
|
256
166
|
|
|
167
|
+
// Inject safety hints for the command being generated (AI sees what's risky)
|
|
168
|
+
const safetyBlock = sessionEntries.length > 0
|
|
169
|
+
? (() => {
|
|
170
|
+
const lastCmd = sessionEntries[sessionEntries.length - 1]?.cmd;
|
|
171
|
+
if (lastCmd) {
|
|
172
|
+
const safetyHints = discoverSafetyHints(lastCmd);
|
|
173
|
+
return safetyHints.length > 0 ? `\n\nLAST COMMAND SAFETY:\n${safetyHints.join("\n")}` : "";
|
|
174
|
+
}
|
|
175
|
+
return "";
|
|
176
|
+
})()
|
|
177
|
+
: "";
|
|
178
|
+
|
|
257
179
|
return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
|
|
258
180
|
The user describes what they want in plain English. You translate to the exact shell command.
|
|
259
181
|
|
|
260
182
|
RULES:
|
|
183
|
+
- SIMPLICITY FIRST: Use the simplest command that works. Prefer grep | sort | head over 10-pipe chains. Complex pipelines are OK when needed, but NEVER pass file:line output to wc or xargs without cleaning it first.
|
|
184
|
+
- ALWAYS use grep -rn (with -r) when searching directories. NEVER use grep without -r on src/ or any directory.
|
|
261
185
|
- When user refers to items from previous output, use the EXACT names shown (e.g., "feature/auth" not "auth", "open-skills" not "open_skills")
|
|
262
186
|
- When user says "the largest/smallest/first/second", look at the previous output to identify the correct item
|
|
263
187
|
- When user says "them all" or "combine them", refer to items from the most recent command output
|
|
@@ -265,6 +189,7 @@ RULES:
|
|
|
265
189
|
- For text search in code, use grep -rn, NOT nm or objdump (those are for compiled binaries)
|
|
266
190
|
- On macOS: for memory use vm_stat or top -l 1, for disk use df -h, for processes use ps aux
|
|
267
191
|
- macOS uses BSD tools, NOT GNU. Use: du -d 1 (not --max-depth), ls (not ls --color), sort -r (not sort --reverse), ps aux (not ps --sort)
|
|
192
|
+
- NEVER use grep -P (PCRE). macOS grep has NO -P flag. Use grep -E for extended regex, or sed/awk for complex extraction.
|
|
268
193
|
- NEVER invent commands that don't exist. Stick to standard Unix/macOS commands.
|
|
269
194
|
- NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
|
|
270
195
|
- NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
|
|
@@ -272,8 +197,10 @@ RULES:
|
|
|
272
197
|
- Use exact file paths from the project context below. Do NOT guess paths.
|
|
273
198
|
- For "what would break if I deleted X": use grep -rn "from.*X\\|import.*X\\|require.*X" src/ to find all importers.
|
|
274
199
|
- For "find where X is defined": use grep -rn "export.*function X\\|export.*class X\\|export.*const X" src/
|
|
275
|
-
- For "show me the code of function X": use grep -A
|
|
200
|
+
- For "show me the code of function X": if you know the file, use grep -A 30 "function X" src/file.ts. If not, use grep -rn -A 30 "function X" src/ --include="*.ts"
|
|
201
|
+
- ALWAYS use grep -rn (recursive) when searching directories. NEVER use grep without -r on a directory — it will fail.
|
|
276
202
|
- For conceptual questions about what code does: use cat on the relevant file, the AI summary will explain it.
|
|
203
|
+
- For DESTRUCTIVE requests (delete, remove, install, push): output BLOCKED: <reason>. NEVER try to execute destructive commands.
|
|
277
204
|
|
|
278
205
|
COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
|
|
279
206
|
- "how many tests and do they pass" → bun test (extract count AND pass/fail from output)
|
|
@@ -287,7 +214,13 @@ BLOCKED ALTERNATIVES: If your preferred command would require installing package
|
|
|
287
214
|
- Security scan → grep -rn "eval\\|exec\\|spawn\\|password\\|secret" src/
|
|
288
215
|
- Dependency audit → cat package.json | grep -A 50 dependencies
|
|
289
216
|
- Test coverage → bun test --coverage (or npm run test:coverage if available)
|
|
290
|
-
NEVER give up. Always try a grep/find/cat read-only alternative.
|
|
217
|
+
NEVER give up. NEVER output BLOCKED for analysis questions. Always try a grep/find/cat/wc/awk read-only alternative.
|
|
218
|
+
- Cyclomatic complexity → grep -rn "if\\|else\\|for\\|while\\|switch\\|case\\|catch\\|&&\\|||" src/ --include="*.ts" | wc -l
|
|
219
|
+
- Unused exports → grep -rn "export function\|export const\|export class" src/ --include="*.ts" | sed 's/.*export [a-z]* //' | sed 's/[(<:].*//' | sort -u
|
|
220
|
+
- Dead code → for each exported name, grep -rn "name" src/ --include="*.ts" | wc -l (if only 1 match = unused)
|
|
221
|
+
- Dependency graph → grep -rn "from " src/ --include="*.ts" | sed 's/:.*from "/→/' | sed 's/".*//' | sort -u
|
|
222
|
+
- Most parameters → grep -rn "function " src/ --include="*.ts" | awk -F'[()]' '{print gsub(/,/,",",$2)+1, $0}' | sort -nr | head -10
|
|
223
|
+
ALWAYS try a heuristic shell approach before giving up. NEVER say BLOCKED for analysis questions.
|
|
291
224
|
|
|
292
225
|
SEMANTIC MAPPING: When the user references a concept, search the file tree for RELATED terms:
|
|
293
226
|
- Look at directory names: src/agent/ likely contains "agentic" code
|
|
@@ -300,7 +233,7 @@ EXISTENCE CHECKS: If the prompt starts with "is there", "does this have", "do we
|
|
|
300
233
|
|
|
301
234
|
MONOREPO: If the project context says "MONOREPO", search packages/ or apps/ NOT src/. Use: grep -rn "pattern" packages/ --include="*.ts". For specific packages, use packages/PKGNAME/src/.
|
|
302
235
|
cwd: ${process.cwd()}
|
|
303
|
-
shell: zsh / macOS${projectContext}${restrictionBlock}${contextBlock}`;
|
|
236
|
+
shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}`;
|
|
304
237
|
}
|
|
305
238
|
|
|
306
239
|
// ── streaming translate ───────────────────────────────────────────────────────
|
package/src/cli.tsx
CHANGED
|
@@ -44,7 +44,9 @@ MCP TOOLS (20+):
|
|
|
44
44
|
snapshot, token_stats, session_history
|
|
45
45
|
|
|
46
46
|
ENVIRONMENT:
|
|
47
|
-
|
|
47
|
+
XAI_API_KEY xAI API key (Grok, code-optimized — default)
|
|
48
|
+
CEREBRAS_API_KEY Cerebras API key (free, open-source)
|
|
49
|
+
GROQ_API_KEY Groq API key (free, ultra-fast inference)
|
|
48
50
|
ANTHROPIC_API_KEY Anthropic API key (Claude models)
|
|
49
51
|
`);
|
|
50
52
|
process.exit(0);
|
|
@@ -371,7 +373,7 @@ else if (args[0] === "history") {
|
|
|
371
373
|
|
|
372
374
|
else if (args[0] === "explain" && args[1]) {
|
|
373
375
|
const command = args.slice(1).join(" ");
|
|
374
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
|
|
376
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY) {
|
|
375
377
|
console.error("explain requires an API key"); process.exit(1);
|
|
376
378
|
}
|
|
377
379
|
const { explainCommand } = await import("./ai.js");
|
|
@@ -400,7 +402,7 @@ else if (args.length > 0) {
|
|
|
400
402
|
// Everything that doesn't match a subcommand is treated as natural language
|
|
401
403
|
const prompt = args.join(" ");
|
|
402
404
|
|
|
403
|
-
const offlineMode = !process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY;
|
|
405
|
+
const offlineMode = !process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY;
|
|
404
406
|
|
|
405
407
|
const { translateToCommand, checkPermissions, isIrreversible } = await import("./ai.js");
|
|
406
408
|
const { execSync } = await import("child_process");
|
|
@@ -458,9 +460,10 @@ else if (args.length > 0) {
|
|
|
458
460
|
} catch {}
|
|
459
461
|
}
|
|
460
462
|
}
|
|
461
|
-
//
|
|
463
|
+
// Show the block reason clearly
|
|
462
464
|
if (e.message?.startsWith("BLOCKED:")) {
|
|
463
|
-
console.log(
|
|
465
|
+
console.log(`⚠ ${e.message}`);
|
|
466
|
+
console.log(` This is a READ-ONLY terminal. Run directly in your shell if you're sure.`);
|
|
464
467
|
} else {
|
|
465
468
|
console.error(e.message);
|
|
466
469
|
}
|
|
@@ -507,7 +510,7 @@ else if (args.length > 0) {
|
|
|
507
510
|
console.error(`[open-terminal] invalid command detected: ${validation.issues.join(", ")}`);
|
|
508
511
|
try {
|
|
509
512
|
const retryCommand = await translateToCommand(
|
|
510
|
-
`${prompt} (
|
|
513
|
+
`${prompt} (Previous command had issues: ${validation.issues.join(", ")}. Fix those specific issues. Keep the approach but correct the errors.)`,
|
|
511
514
|
perms, []
|
|
512
515
|
);
|
|
513
516
|
if (retryCommand && retryCommand !== command) {
|
|
@@ -515,6 +518,14 @@ else if (args.length > 0) {
|
|
|
515
518
|
if (retryValidation.valid || retryValidation.issues.length < validation.issues.length) {
|
|
516
519
|
command = retryCommand;
|
|
517
520
|
console.error(`[open-terminal] retried: $ ${command}`);
|
|
521
|
+
} else {
|
|
522
|
+
// Retry also invalid — use the simpler of the two
|
|
523
|
+
const retryPipes = (retryCommand.match(/\|/g) || []).length;
|
|
524
|
+
const origPipes = (command.match(/\|/g) || []).length;
|
|
525
|
+
if (retryPipes < origPipes) {
|
|
526
|
+
command = retryCommand;
|
|
527
|
+
console.error(`[open-terminal] retried (simpler): $ ${command}`);
|
|
528
|
+
}
|
|
518
529
|
}
|
|
519
530
|
}
|
|
520
531
|
} catch {}
|
|
@@ -642,11 +653,13 @@ else if (args.length > 0) {
|
|
|
642
653
|
// ── TUI mode (no args) ──────────────────────────────────────────────────────
|
|
643
654
|
|
|
644
655
|
else {
|
|
645
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY) {
|
|
656
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.CEREBRAS_API_KEY && !process.env.GROQ_API_KEY && !process.env.XAI_API_KEY) {
|
|
646
657
|
console.error("terminal: No API key found.");
|
|
647
658
|
console.error("Set one of:");
|
|
648
|
-
console.error(" export
|
|
649
|
-
console.error(" export
|
|
659
|
+
console.error(" export XAI_API_KEY=your_key (Grok, code-optimized — default)");
|
|
660
|
+
console.error(" export CEREBRAS_API_KEY=your_key (free, open-source)");
|
|
661
|
+
console.error(" export GROQ_API_KEY=your_key (free, ultra-fast)");
|
|
662
|
+
console.error(" export ANTHROPIC_API_KEY=your_key (Claude)");
|
|
650
663
|
process.exit(1);
|
|
651
664
|
}
|
|
652
665
|
|
package/src/command-validator.ts
CHANGED
|
@@ -78,6 +78,17 @@ export function validateCommand(command: string, cwd: string): ValidationResult
|
|
|
78
78
|
issues.push(`GNU flag on macOS: ${gnuFlags.join(", ")}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Complexity guard — extreme pipe chains are fragile
|
|
82
|
+
const pipeCount = (command.match(/\|/g) || []).length;
|
|
83
|
+
if (pipeCount > 7) {
|
|
84
|
+
issues.push(`too complex: ${pipeCount} pipes — simplify`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// grep -P (PCRE) doesn't exist on macOS
|
|
88
|
+
if (/grep\s+.*-[a-zA-Z]*P/.test(command)) {
|
|
89
|
+
issues.push("grep -P (PCRE) not available on macOS — use grep -E");
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
return {
|
|
82
93
|
valid: issues.length === 0,
|
|
83
94
|
issues,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Context hints — discover context via lightweight checks, inject into AI prompt
|
|
2
|
+
// Regex DISCOVERS, AI DECIDES. No hardcoded logic that makes decisions.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
export interface ContextHints {
|
|
8
|
+
project: string[]; // project metadata
|
|
9
|
+
output: string[]; // observations about command output
|
|
10
|
+
safety: string[]; // safety-relevant observations
|
|
11
|
+
environment: string[]; // system/env observations
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Discover project context from the filesystem */
|
|
15
|
+
export function discoverProjectHints(cwd: string): string[] {
|
|
16
|
+
const hints: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Package managers and project files
|
|
19
|
+
const projectFiles: [string, string][] = [
|
|
20
|
+
["package.json", "Node.js/TypeScript"],
|
|
21
|
+
["pyproject.toml", "Python"],
|
|
22
|
+
["requirements.txt", "Python"],
|
|
23
|
+
["go.mod", "Go"],
|
|
24
|
+
["Cargo.toml", "Rust"],
|
|
25
|
+
["pom.xml", "Java/Maven"],
|
|
26
|
+
["build.gradle", "Java/Gradle"],
|
|
27
|
+
["build.gradle.kts", "Java/Gradle (Kotlin DSL)"],
|
|
28
|
+
["Makefile", "Has Makefile"],
|
|
29
|
+
["Dockerfile", "Has Docker"],
|
|
30
|
+
["docker-compose.yml", "Has Docker Compose"],
|
|
31
|
+
["docker-compose.yaml", "Has Docker Compose"],
|
|
32
|
+
[".github/workflows", "Has GitHub Actions CI"],
|
|
33
|
+
["Gemfile", "Ruby"],
|
|
34
|
+
["composer.json", "PHP"],
|
|
35
|
+
["mix.exs", "Elixir"],
|
|
36
|
+
["build.zig", "Zig"],
|
|
37
|
+
["CMakeLists.txt", "C/C++ (CMake)"],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const [file, lang] of projectFiles) {
|
|
41
|
+
if (existsSync(join(cwd, file))) {
|
|
42
|
+
hints.push(`Project type: ${lang} (${file} found)`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract rich metadata from package.json
|
|
47
|
+
const pkgPath = join(cwd, "package.json");
|
|
48
|
+
if (existsSync(pkgPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
51
|
+
if (pkg.name) hints.push(`Package name: ${pkg.name}@${pkg.version ?? "unknown"}`);
|
|
52
|
+
if (pkg.scripts) {
|
|
53
|
+
hints.push(`Available scripts: ${Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).slice(0, 10).join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
if (pkg.dependencies) hints.push(`Dependencies: ${Object.keys(pkg.dependencies).join(", ")}`);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract from pyproject.toml
|
|
60
|
+
const pyPath = join(cwd, "pyproject.toml");
|
|
61
|
+
if (existsSync(pyPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const py = readFileSync(pyPath, "utf8");
|
|
64
|
+
const name = py.match(/name\s*=\s*"([^"]+)"/)?.[1];
|
|
65
|
+
if (name) hints.push(`Python package: ${name}`);
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract from go.mod
|
|
70
|
+
const goPath = join(cwd, "go.mod");
|
|
71
|
+
if (existsSync(goPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const go = readFileSync(goPath, "utf8");
|
|
74
|
+
const mod = go.match(/module\s+(\S+)/)?.[1];
|
|
75
|
+
if (mod) hints.push(`Go module: ${mod}`);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract from Cargo.toml
|
|
80
|
+
const cargoPath = join(cwd, "Cargo.toml");
|
|
81
|
+
if (existsSync(cargoPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const cargo = readFileSync(cargoPath, "utf8");
|
|
84
|
+
const name = cargo.match(/name\s*=\s*"([^"]+)"/)?.[1];
|
|
85
|
+
if (name) hints.push(`Rust crate: ${name}`);
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Monorepo detection
|
|
90
|
+
if (existsSync(join(cwd, "packages"))) {
|
|
91
|
+
try {
|
|
92
|
+
const pkgs = readdirSync(join(cwd, "packages")).filter(d => !d.startsWith("."));
|
|
93
|
+
hints.push(`MONOREPO: ${pkgs.length} packages in packages/ — search packages/ not src/`);
|
|
94
|
+
hints.push(`Packages: ${pkgs.slice(0, 10).join(", ")}`);
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
if (existsSync(join(cwd, "apps"))) {
|
|
98
|
+
hints.push("MONOREPO: apps/ directory detected");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Makefile targets
|
|
102
|
+
if (existsSync(join(cwd, "Makefile"))) {
|
|
103
|
+
try {
|
|
104
|
+
const { execSync } = require("child_process");
|
|
105
|
+
const targets = execSync("grep -E '^[a-zA-Z_-]+:' Makefile | head -10 | cut -d: -f1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
106
|
+
if (targets) hints.push(`Makefile targets: ${targets.split("\n").join(", ")}`);
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Source directory structure
|
|
111
|
+
try {
|
|
112
|
+
const { execSync } = require("child_process");
|
|
113
|
+
const srcDirs = ["src", "lib", "app", "packages"];
|
|
114
|
+
for (const dir of srcDirs) {
|
|
115
|
+
if (existsSync(join(cwd, dir))) {
|
|
116
|
+
const tree = execSync(
|
|
117
|
+
`find ${dir} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/dist/*' -not -name '*.test.*' 2>/dev/null | sort | head -60`,
|
|
118
|
+
{ cwd, encoding: "utf8", timeout: 3000 }
|
|
119
|
+
).trim();
|
|
120
|
+
if (tree) hints.push(`Files in ${dir}/:\n${tree}`);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Top-level files
|
|
125
|
+
const topLevel = execSync("ls -1", { cwd, encoding: "utf8", timeout: 1000 }).trim();
|
|
126
|
+
hints.push(`Top-level: ${topLevel.split("\n").join(", ")}`);
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
return hints;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Discover output-specific hints (observations about command output) */
|
|
133
|
+
export function discoverOutputHints(output: string, command: string): string[] {
|
|
134
|
+
const hints: string[] = [];
|
|
135
|
+
|
|
136
|
+
const lines = output.split("\n");
|
|
137
|
+
hints.push(`Output: ${lines.length} lines, ${output.length} chars`);
|
|
138
|
+
|
|
139
|
+
// Only detect test results from actual test runners (not grep output containing "pass"/"fail" in code)
|
|
140
|
+
const isGrepOutput = /^\s*(src\/|\.\/|packages\/).*:\d+:/.test(output);
|
|
141
|
+
if (!isGrepOutput) {
|
|
142
|
+
const passMatch = output.match(/(\d+)\s+pass(?:ed|ing)?\b/i);
|
|
143
|
+
const failMatch = output.match(/(\d+)\s+fail(?:ed|ing|ure)?\b/i);
|
|
144
|
+
if (passMatch) hints.push(`Test results detected: ${passMatch[0]}`);
|
|
145
|
+
if (failMatch) hints.push(`Test results detected: ${failMatch[0]}`);
|
|
146
|
+
|
|
147
|
+
// Error patterns (only from actual command output, not code search)
|
|
148
|
+
if (output.match(/error\s*TS\d+/i)) hints.push("TypeScript errors detected in output");
|
|
149
|
+
if (output.match(/ENOENT|EACCES|EADDRINUSE/)) hints.push("System error code detected in output");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Coverage patterns
|
|
153
|
+
if (output.match(/%\s*Funcs|%\s*Lines|coverage/i)) hints.push("Code coverage data detected in output");
|
|
154
|
+
|
|
155
|
+
// Large/repetitive output
|
|
156
|
+
if (lines.length > 100) hints.push(`Large output (${lines.length} lines) — consider summarizing`);
|
|
157
|
+
const uniqueLines = new Set(lines.map(l => l.trim())).size;
|
|
158
|
+
if (uniqueLines < lines.length * 0.5) hints.push("Output has many duplicate/similar lines");
|
|
159
|
+
|
|
160
|
+
// Sensitive data (only env var assignments, not code containing the word KEY/TOKEN)
|
|
161
|
+
if (output.match(/^[A-Z_]+(KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S+/m)) hints.push("Output may contain sensitive data — redact credentials");
|
|
162
|
+
|
|
163
|
+
return hints;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Discover safety hints about a command */
|
|
167
|
+
export function discoverSafetyHints(command: string): string[] {
|
|
168
|
+
const hints: string[] = [];
|
|
169
|
+
|
|
170
|
+
// Observations about the command (AI decides if it's safe)
|
|
171
|
+
if (command.match(/\brm\b|\brmdir\b|\btruncate\b/)) hints.push("SAFETY: command contains file deletion (rm/rmdir/truncate)");
|
|
172
|
+
if (command.match(/\bkill\b|\bkillall\b|\bpkill\b/)) hints.push("SAFETY: command kills processes");
|
|
173
|
+
if (command.match(/\bgit\s+push\b|\bgit\s+reset\s+--hard\b/)) hints.push("SAFETY: command pushes/resets git");
|
|
174
|
+
if (command.match(/\bnpx\b|\bnpm\s+install\b|\bpip\s+install\b/)) hints.push("SAFETY: command installs packages");
|
|
175
|
+
if (command.match(/\bsed\s+-i\b|\bcodemod\b/)) hints.push("SAFETY: command modifies files in-place");
|
|
176
|
+
if (command.match(/\btouch\b|\bmkdir\b/)) hints.push("SAFETY: command creates files/directories");
|
|
177
|
+
if (command.match(/>\s*\S+\.\w+/)) hints.push("SAFETY: command writes to a file via redirect");
|
|
178
|
+
if (command.match(/\b(bun|npm|pnpm)\s+run\s+dev\b|\bstart\b/)) hints.push("SAFETY: command starts a server/process");
|
|
179
|
+
|
|
180
|
+
// Read-only observations
|
|
181
|
+
if (command.match(/^\s*git\s+(log|show|diff|status|branch|blame|tag)\b/)) hints.push("This is a read-only git command");
|
|
182
|
+
if (command.match(/^\s*(ls|cat|head|tail|grep|find|wc|du|df|uptime|whoami|pwd)\b/)) hints.push("This is a read-only command");
|
|
183
|
+
|
|
184
|
+
return hints;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Format all hints for system prompt injection */
|
|
188
|
+
export function formatHints(project: string[], output?: string[], safety?: string[]): string {
|
|
189
|
+
const sections: string[] = [];
|
|
190
|
+
|
|
191
|
+
if (project.length > 0) {
|
|
192
|
+
sections.push("PROJECT CONTEXT:\n" + project.join("\n"));
|
|
193
|
+
}
|
|
194
|
+
if (output && output.length > 0) {
|
|
195
|
+
sections.push("OUTPUT OBSERVATIONS:\n" + output.join("\n"));
|
|
196
|
+
}
|
|
197
|
+
if (safety && safety.length > 0) {
|
|
198
|
+
sections.push("SAFETY OBSERVATIONS:\n" + safety.join("\n"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return sections.join("\n\n");
|
|
202
|
+
}
|
package/src/output-processor.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { getProvider } from "./providers/index.js";
|
|
5
5
|
import { estimateTokens } from "./parsers/index.js";
|
|
6
6
|
import { recordSaving } from "./economy.js";
|
|
7
|
+
import { discoverOutputHints } from "./context-hints.js";
|
|
7
8
|
|
|
8
9
|
export interface ProcessedOutput {
|
|
9
10
|
/** AI-generated summary (concise, structured) */
|
|
@@ -79,27 +80,15 @@ export async function processOutput(
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
try {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (passMatch && failMatch && originalPrompt && /test|pass|fail/i.test(originalPrompt)) {
|
|
88
|
-
const passed = parseInt(passMatch[1]);
|
|
89
|
-
const failed = parseInt(failMatch[1]);
|
|
90
|
-
const answer = failed === 0
|
|
91
|
-
? `✓ Yes, all ${passed} tests pass.`
|
|
92
|
-
: `✗ ${failed} of ${passed + failed} tests failed.`;
|
|
93
|
-
const savedTokens = estimateTokens(output) - estimateTokens(answer);
|
|
94
|
-
return {
|
|
95
|
-
summary: answer, full: output, tokensSaved: Math.max(0, savedTokens),
|
|
96
|
-
aiTokensUsed: 0, aiProcessed: true, aiCostUsd: 0, savingsValueUsd: 0, netSavingsUsd: 0,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
83
|
+
// Discover output hints — regex discovers patterns, AI decides what matters
|
|
84
|
+
const outputHints = discoverOutputHints(output, command);
|
|
85
|
+
const hintsBlock = outputHints.length > 0
|
|
86
|
+
? `\n\nOUTPUT OBSERVATIONS:\n${outputHints.join("\n")}`
|
|
87
|
+
: "";
|
|
99
88
|
|
|
100
89
|
const provider = getProvider();
|
|
101
90
|
const summary = await provider.complete(
|
|
102
|
-
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`,
|
|
91
|
+
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}`,
|
|
103
92
|
{
|
|
104
93
|
system: SUMMARIZE_PROMPT,
|
|
105
94
|
maxTokens: 300,
|