@hasna/terminal 4.3.1 → 4.3.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.
Files changed (81) hide show
  1. package/dist/App.js +404 -0
  2. package/dist/Browse.js +79 -0
  3. package/dist/FuzzyPicker.js +47 -0
  4. package/dist/Onboarding.js +51 -0
  5. package/dist/Spinner.js +12 -0
  6. package/dist/StatusBar.js +49 -0
  7. package/dist/ai.js +316 -0
  8. package/dist/cache.js +42 -0
  9. package/dist/cli.js +778 -0
  10. package/dist/command-rewriter.js +64 -0
  11. package/dist/command-validator.js +86 -0
  12. package/dist/compression.js +91 -0
  13. package/dist/context-hints.js +285 -0
  14. package/dist/db/pg-migrations.js +70 -0
  15. package/dist/diff-cache.js +107 -0
  16. package/dist/discover.js +212 -0
  17. package/dist/economy.js +155 -0
  18. package/dist/expand-store.js +44 -0
  19. package/dist/file-cache.js +72 -0
  20. package/dist/file-index.js +62 -0
  21. package/dist/history.js +62 -0
  22. package/dist/lazy-executor.js +54 -0
  23. package/dist/line-dedup.js +59 -0
  24. package/dist/loop-detector.js +75 -0
  25. package/dist/mcp/install.js +189 -0
  26. package/dist/mcp/server.js +90 -0
  27. package/dist/mcp/tools/batch.js +111 -0
  28. package/dist/mcp/tools/execute.js +194 -0
  29. package/dist/mcp/tools/files.js +290 -0
  30. package/dist/mcp/tools/git.js +233 -0
  31. package/dist/mcp/tools/helpers.js +63 -0
  32. package/dist/mcp/tools/memory.js +151 -0
  33. package/dist/mcp/tools/meta.js +138 -0
  34. package/dist/mcp/tools/process.js +50 -0
  35. package/dist/mcp/tools/project.js +251 -0
  36. package/dist/mcp/tools/search.js +86 -0
  37. package/dist/noise-filter.js +94 -0
  38. package/dist/output-processor.js +233 -0
  39. package/dist/output-store.js +112 -0
  40. package/dist/paths.js +28 -0
  41. package/dist/providers/anthropic.js +43 -0
  42. package/dist/providers/base.js +4 -0
  43. package/dist/providers/cerebras.js +8 -0
  44. package/dist/providers/groq.js +8 -0
  45. package/dist/providers/index.js +142 -0
  46. package/dist/providers/openai-compat.js +93 -0
  47. package/dist/providers/xai.js +8 -0
  48. package/dist/recipes/model.js +20 -0
  49. package/dist/recipes/storage.js +153 -0
  50. package/dist/search/content-search.js +70 -0
  51. package/dist/search/file-search.js +61 -0
  52. package/dist/search/filters.js +34 -0
  53. package/dist/search/index.js +5 -0
  54. package/dist/search/semantic.js +346 -0
  55. package/dist/session-boot.js +59 -0
  56. package/dist/session-context.js +55 -0
  57. package/dist/sessions-db.js +240 -0
  58. package/dist/smart-display.js +286 -0
  59. package/dist/snapshots.js +51 -0
  60. package/dist/supervisor.js +112 -0
  61. package/dist/test-watchlist.js +131 -0
  62. package/dist/tokens.js +17 -0
  63. package/dist/tool-profiles.js +130 -0
  64. package/dist/tree.js +94 -0
  65. package/dist/usage-cache.js +65 -0
  66. package/package.json +2 -1
  67. package/src/Onboarding.tsx +1 -1
  68. package/src/ai.ts +5 -4
  69. package/src/cache.ts +2 -2
  70. package/src/db/pg-migrations.ts +77 -0
  71. package/src/economy.ts +3 -3
  72. package/src/history.ts +2 -2
  73. package/src/mcp/server.ts +55 -0
  74. package/src/mcp/tools/memory.ts +4 -2
  75. package/src/output-store.ts +2 -1
  76. package/src/paths.ts +32 -0
  77. package/src/recipes/storage.ts +3 -3
  78. package/src/session-context.ts +2 -2
  79. package/src/sessions-db.ts +15 -4
  80. package/src/tool-profiles.ts +4 -3
  81. package/src/usage-cache.ts +2 -2
package/dist/ai.js ADDED
@@ -0,0 +1,316 @@
1
+ import { cacheGet, cacheSet } from "./cache.js";
2
+ import { getProvider } from "./providers/index.js";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { getTerminalDir } from "./paths.js";
6
+ import { discoverProjectHints, discoverSafetyHints, formatHints } from "./context-hints.js";
7
+ // ── model routing ─────────────────────────────────────────────────────────────
8
+ // Config-driven model selection. Defaults per provider, user can override in ~/.hasna/terminal/config.json
9
+ const COMPLEX_SIGNALS = [
10
+ /\b(undo|revert|rollback|previous|last)\b/i,
11
+ /\b(all files?|recursively|bulk|batch)\b/i,
12
+ /\b(pipeline|chain|then|and then|after)\b/i,
13
+ /\b(if|when|unless|only if)\b/i,
14
+ /\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i,
15
+ /\b(inside|within|under)\b/i,
16
+ /[|&;]{2}/,
17
+ ];
18
+ /** Default models per provider — user can override in ~/.hasna/terminal/config.json under "models" */
19
+ const MODEL_DEFAULTS = {
20
+ cerebras: { fast: "qwen-3-235b-a22b-instruct-2507", smart: "qwen-3-235b-a22b-instruct-2507" },
21
+ groq: { fast: "openai/gpt-oss-120b", smart: "moonshotai/kimi-k2-instruct" },
22
+ xai: { fast: "grok-code-fast-1", smart: "grok-4-fast-non-reasoning" },
23
+ anthropic: { fast: "claude-haiku-4-5-20251001", smart: "claude-sonnet-4-6" },
24
+ };
25
+ /** Load user model overrides from ~/.hasna/terminal/config.json (cached 30s) */
26
+ let _modelOverrides = null;
27
+ let _modelOverridesAt = 0;
28
+ function loadModelOverrides() {
29
+ const now = Date.now();
30
+ if (_modelOverrides && now - _modelOverridesAt < 30_000)
31
+ return _modelOverrides;
32
+ try {
33
+ const configPath = join(getTerminalDir(), "config.json");
34
+ if (existsSync(configPath)) {
35
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
36
+ _modelOverrides = config.models ?? {};
37
+ _modelOverridesAt = now;
38
+ return _modelOverrides;
39
+ }
40
+ }
41
+ catch { }
42
+ _modelOverrides = {};
43
+ _modelOverridesAt = now;
44
+ return _modelOverrides;
45
+ }
46
+ /** Model routing per provider — config-driven with defaults */
47
+ function pickModel(nl) {
48
+ const isComplex = COMPLEX_SIGNALS.some((r) => r.test(nl)) || nl.split(" ").length > 10;
49
+ const provider = getProvider();
50
+ const defaults = MODEL_DEFAULTS[provider.name] ?? MODEL_DEFAULTS.cerebras;
51
+ const overrides = loadModelOverrides()[provider.name] ?? {};
52
+ return {
53
+ fast: overrides.fast ?? defaults.fast,
54
+ smart: overrides.smart ?? defaults.smart,
55
+ pick: isComplex ? "smart" : "fast",
56
+ };
57
+ }
58
+ // ── irreversibility ───────────────────────────────────────────────────────────
59
+ const IRREVERSIBLE_PATTERNS = [
60
+ /\brm\s/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i,
61
+ /\bdelete\s+from\b/i, /\bmv\b.*\/dev\/null/, /\becho\b.*>\s*[^>]/, /\bcat\b.*>\s*[^>]/,
62
+ /\bdd\b/, /\bmkfs\b/, /\bformat\b/, /\bshred\b/,
63
+ // Process/service killing
64
+ /\bkill\b/, /\bkillall\b/, /\bpkill\b/,
65
+ // Git push/force operations
66
+ /\bgit\s+push\b/, /\bgit\s+reset\s+--hard\b/, /\bgit\s+force\b/,
67
+ // Code modification / package installation (security risk)
68
+ /\bnpx\s+\S+/, /\bnpm\s+install\b/, /\bbun\s+add\b/, /\bpip\s+install\b/,
69
+ /\bcodemod\b/, /\bsed\s+-i\b/, /\bawk\s.*>\s*\S+\.\w+/, /\bperl\s+-[pi]\b/,
70
+ // File creation/modification (READ-ONLY terminal)
71
+ /\btouch\b/, /\bmkdir\b/, /\becho\s.*>/, /\btee\b/, /\bcp\b/, /\bmv\b/,
72
+ // Starting servers/processes (dangerous from NL)
73
+ /\b(bun|npm|pnpm|yarn)\s+run\s+dev\b/, /\b(bun|npm)\s+start\b/,
74
+ ];
75
+ // Commands that are ALWAYS safe (read-only git, etc.)
76
+ const SAFE_OVERRIDES = [
77
+ /^\s*git\s+(log|show|diff|branch|status|blame|tag|remote|stash\s+list)\b/,
78
+ /^\s*git\s+log\b/,
79
+ // find -exec with read-only tools is safe
80
+ /\bfind\b.*-exec\s+(wc|cat|head|tail|grep|stat|file|du|ls)\b/,
81
+ // find without -exec is always safe
82
+ /^\s*find\b(?!.*-exec\s+(rm|mv|chmod|chown|sed))/,
83
+ // xargs with read-only tools is safe
84
+ /\bxargs\s+(wc|cat|head|tail|grep|stat|file|du|ls|git\s+log|git\s+show|git\s+blame)\b/,
85
+ /\bxargs\s+-I\s*\S+\s+(wc|cat|head|tail|grep|stat|git)\b/,
86
+ ];
87
+ export function isIrreversible(command) {
88
+ // Safe overrides take priority
89
+ if (SAFE_OVERRIDES.some((r) => r.test(command)))
90
+ return false;
91
+ return IRREVERSIBLE_PATTERNS.some((r) => r.test(command));
92
+ }
93
+ // ── permissions ───────────────────────────────────────────────────────────────
94
+ const DESTRUCTIVE_PATTERNS = [/\brm\b/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i, /\bdelete\s+from\b/i];
95
+ const NETWORK_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bssh\b/, /\bscp\b/, /\bping\b/, /\bnc\b/, /\bnetcat\b/];
96
+ const SUDO_PATTERNS = [/\bsudo\b/];
97
+ const INSTALL_PATTERNS = [/\bbrew\s+install\b/, /\bnpm\s+install\s+-g\b/, /\bpip\s+install\b/, /\bapt\s+install\b/, /\byum\s+install\b/];
98
+ const WRITE_OUTSIDE_PATTERNS = [/\s(\/etc|\/usr|\/var|\/opt|\/root|~\/[^.])/, />\s*\//];
99
+ export function checkPermissions(command, perms) {
100
+ if (!perms.destructive && DESTRUCTIVE_PATTERNS.some((r) => r.test(command)))
101
+ return "destructive commands are disabled";
102
+ if (!perms.network && NETWORK_PATTERNS.some((r) => r.test(command)))
103
+ return "network commands are disabled";
104
+ if (!perms.sudo && SUDO_PATTERNS.some((r) => r.test(command)))
105
+ return "sudo is disabled";
106
+ if (!perms.install && INSTALL_PATTERNS.some((r) => r.test(command)))
107
+ return "package installation is disabled";
108
+ if (!perms.write_outside_cwd && WRITE_OUTSIDE_PATTERNS.some((r) => r.test(command)))
109
+ return "writing outside cwd is disabled";
110
+ return null;
111
+ }
112
+ // ── correction memory ───────────────────────────────────────────────────────
113
+ /** Load past corrections relevant to a prompt — injected as negative examples */
114
+ function loadCorrectionHints(prompt) {
115
+ try {
116
+ // Dynamic import to avoid circular deps
117
+ const { findSimilarCorrections } = require("./sessions-db.js");
118
+ const corrections = findSimilarCorrections(prompt, 3);
119
+ if (corrections.length === 0)
120
+ return "";
121
+ const lines = corrections.map((c) => `AVOID: "${c.failed_command}" (failed: ${c.error_type}). USE: "${c.corrected_command}" instead.`);
122
+ return `\n\nLEARNED CORRECTIONS (from past failures):\n${lines.join("\n")}`;
123
+ }
124
+ catch {
125
+ return "";
126
+ }
127
+ }
128
+ // ── project context (powered by context-hints) ──────────────────────────────
129
+ function detectProjectContext() {
130
+ const hints = discoverProjectHints(process.cwd());
131
+ return hints.length > 0 ? `\n\n${formatHints(hints)}` : "";
132
+ }
133
+ // ── system prompt ─────────────────────────────────────────────────────────────
134
+ function buildSystemPrompt(perms, sessionEntries, currentPrompt) {
135
+ const nl = currentPrompt?.toLowerCase() ?? "";
136
+ const restrictions = [];
137
+ if (!perms.destructive)
138
+ restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
139
+ if (!perms.network)
140
+ restrictions.push("- NEVER generate commands that make network requests (curl, wget, ssh, etc.)");
141
+ if (!perms.sudo)
142
+ restrictions.push("- NEVER generate commands requiring sudo");
143
+ if (!perms.write_outside_cwd)
144
+ restrictions.push("- NEVER write to paths outside the current working directory");
145
+ if (!perms.install)
146
+ restrictions.push("- NEVER install packages (brew, npm -g, pip, apt, etc.)");
147
+ const restrictionBlock = restrictions.length > 0
148
+ ? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
149
+ : "";
150
+ let contextBlock = "";
151
+ if (sessionEntries.length > 0) {
152
+ const lines = [];
153
+ for (const e of sessionEntries.slice(-5)) {
154
+ lines.push(`> ${e.nl}`);
155
+ lines.push(`$ ${e.cmd}`);
156
+ if (e.output)
157
+ lines.push(e.output);
158
+ if (e.error)
159
+ lines.push("(command failed)");
160
+ }
161
+ contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
162
+ }
163
+ const projectContext = detectProjectContext();
164
+ const safetyBlock = sessionEntries.length > 0
165
+ ? (() => {
166
+ const lastCmd = sessionEntries[sessionEntries.length - 1]?.cmd;
167
+ if (lastCmd) {
168
+ const safetyHints = discoverSafetyHints(lastCmd);
169
+ return safetyHints.length > 0 ? `\n\nLAST COMMAND SAFETY:\n${safetyHints.join("\n")}` : "";
170
+ }
171
+ return "";
172
+ })()
173
+ : "";
174
+ // ── Conditional sections (only included when relevant) ──
175
+ const wantsStructure = /\b(function|class|interface|export|symbol|structure|hierarchy|outline)\b/i.test(nl);
176
+ const astBlock = wantsStructure ? `\nAST-POWERED QUERIES: For code STRUCTURE questions, use "terminal symbols" instead of grep. It uses AST parsing for TypeScript, Python, Go, Rust.` : "";
177
+ const wantsMultiple = /\b(and|both|also|plus|as well)\b/i.test(nl);
178
+ const compoundBlock = wantsMultiple ? `\nCOMPOUND QUESTIONS: Prefer ONE command that captures all info. NEVER split into separate expensive commands.` : "";
179
+ const wantsAnalysis = /\b(quality|lint|coverage|complexity|unused|dead code|security|audit|scan|dependency)\b/i.test(nl);
180
+ const blockedAltBlock = wantsAnalysis ? `\nBLOCKED ALTERNATIVES: If your preferred command needs installing packages, try READ-ONLY alternatives (grep, cat, wc, awk). NEVER give up on analysis questions.` : "";
181
+ return `Translate to bash. One command. Simplest form. No explanation.
182
+
183
+ list files in current directory → ls
184
+ list all files including hidden → ls -a
185
+ show open files → lsof
186
+ show file size → du -sh file
187
+ show file type → file filename
188
+ show file permissions → ls -la file
189
+ display routing table → route
190
+ show last logged in users → last
191
+ show file stats → stat file
192
+ print directory tree 2 levels → tree -L 2
193
+ count word occurrences in file → grep -c "word" file
194
+ print number of files in dir → ls -1 | wc -l
195
+ print first line of file → head -1 file
196
+ print last line of file → tail -1 file
197
+ print lines 3 to 5 of file → sed -n '3,5p' file
198
+ print every other line → awk 'NR%2==1' file
199
+ count words in file → wc -w file
200
+ find empty files not in subdirs → find . -maxdepth 1 -type f -empty
201
+ show system load → w
202
+ system utilization stats → vmstat
203
+ DNS servers → cat /etc/resolv.conf | grep nameserver
204
+ long integer size → getconf LONG_BIT
205
+ base64 decode string → echo 'str' | base64 -d
206
+ show file owner → ls -la file
207
+ unique lines in file → uniq file
208
+ max cpu time → ulimit -t
209
+ memory info → lsmem
210
+ process priority → nice
211
+ bash profile → cat ~/.bashrc
212
+ search recursively → grep -rn "pattern" src/
213
+ ${astBlock}${compoundBlock}${blockedAltBlock}
214
+ cwd: ${process.cwd()}
215
+ shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}
216
+
217
+ Q:`;
218
+ }
219
+ // ── streaming translate ───────────────────────────────────────────────────────
220
+ export async function translateToCommand(nl, perms, sessionEntries, onToken) {
221
+ // Only use cache when there's no session context (context makes same NL produce different commands)
222
+ if (sessionEntries.length === 0) {
223
+ const cached = cacheGet(nl);
224
+ if (cached) {
225
+ onToken?.(cached);
226
+ return cached;
227
+ }
228
+ }
229
+ const provider = getProvider();
230
+ const routing = pickModel(nl);
231
+ const model = routing.pick === "smart" ? routing.smart : routing.fast;
232
+ const system = buildSystemPrompt(perms, sessionEntries, nl);
233
+ let text;
234
+ if (onToken) {
235
+ text = await provider.stream(nl, { model, maxTokens: 256, temperature: 0, stop: ["\n"], system }, {
236
+ onToken: (partial) => onToken(partial),
237
+ });
238
+ }
239
+ else {
240
+ text = await provider.complete(nl, { model, maxTokens: 256, temperature: 0, stop: ["\n"], system });
241
+ }
242
+ if (text.startsWith("BLOCKED:"))
243
+ throw new Error(text);
244
+ // Strip AI reasoning — extract ONLY the shell command (first line)
245
+ let cleaned = text.trim();
246
+ // Remove ALL markdown code blocks and their content markers
247
+ cleaned = cleaned.replace(/```(?:bash|sh|shell)?\n?/g, "").replace(/```/g, "");
248
+ // Split into lines and find the FIRST one that looks like a SHELL COMMAND
249
+ const lines = cleaned.split("\n");
250
+ let command = "";
251
+ for (const line of lines) {
252
+ const t = line.trim();
253
+ if (!t)
254
+ continue;
255
+ // Skip lines that are clearly English prose, not commands
256
+ if (/^(Based on|I |This |The |Let me|Here|Note:|Since|Looking|To |However|BLOCKED:|If |You |We |For |It |A |An |That )/.test(t))
257
+ continue;
258
+ if (/^[A-Z][a-z].*[.;:!?,]/.test(t))
259
+ continue; // English sentence with punctuation anywhere
260
+ if (t.split(" ").length > 15 && !/[|&;><$]/.test(t))
261
+ continue; // Long line without shell operators = prose
262
+ // Must start with a plausible command character (lowercase, /, ., $, or common tool)
263
+ if (/^[a-z./$~(]/.test(t) || /^[A-Z]+[_=]/.test(t)) {
264
+ command = t;
265
+ break;
266
+ }
267
+ }
268
+ cleaned = command || lines[0]?.trim() || cleaned;
269
+ cacheSet(nl, cleaned);
270
+ return cleaned;
271
+ }
272
+ // ── prefetch ──────────────────────────────────────────────────────────────────
273
+ export function prefetchNext(lastNl, perms, sessionEntries) {
274
+ if (sessionEntries.length === 0 && cacheGet(lastNl))
275
+ return;
276
+ translateToCommand(lastNl, perms, sessionEntries).catch(() => { });
277
+ }
278
+ // ── explain ───────────────────────────────────────────────────────────────────
279
+ export async function explainCommand(command) {
280
+ const provider = getProvider();
281
+ const routing = pickModel("explain"); // simple = fast model
282
+ return provider.complete(command, {
283
+ model: routing.fast,
284
+ maxTokens: 128,
285
+ temperature: 0,
286
+ system: "Explain what this shell command does in one plain English sentence. No markdown, no code blocks.",
287
+ });
288
+ }
289
+ // ── auto-fix ──────────────────────────────────────────────────────────────────
290
+ export async function fixCommand(originalNl, failedCommand, errorOutput, perms, _sessionEntries) {
291
+ const provider = getProvider();
292
+ const routing = pickModel(originalNl);
293
+ // Lightweight fix prompt — no full project context, just rules + restrictions
294
+ const restrictions = [];
295
+ if (!perms.destructive)
296
+ restrictions.push("- NEVER delete/remove/overwrite files");
297
+ if (!perms.network)
298
+ restrictions.push("- NEVER make network requests");
299
+ if (!perms.install)
300
+ restrictions.push("- NEVER install packages");
301
+ const fixSystem = `You are a terminal assistant. Output ONLY the corrected shell command — no explanation.
302
+ macOS/BSD tools. NEVER use grep -P. Use grep -E for extended regex.
303
+ NEVER install packages. READ-ONLY terminal.
304
+ cwd: ${process.cwd()}${restrictions.length > 0 ? `\nRESTRICTIONS:\n${restrictions.join("\n")}` : ""}`;
305
+ const text = await provider.complete(`I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput.slice(0, 2000)}\n\nGive me the corrected command only.`, {
306
+ model: routing.smart,
307
+ maxTokens: 256,
308
+ temperature: 0,
309
+ stop: ["\n"],
310
+ system: fixSystem,
311
+ });
312
+ if (text.startsWith("BLOCKED:"))
313
+ throw new Error(text);
314
+ return text.trim();
315
+ }
316
+ // summarizeOutput() removed — all output processing goes through processOutput() in output-processor.ts
package/dist/cache.js ADDED
@@ -0,0 +1,42 @@
1
+ // In-memory LRU cache + disk persistence for command translations
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { getTerminalDir } from "./paths.js";
5
+ const CACHE_FILE = join(getTerminalDir(), "cache.json");
6
+ const MAX_ENTRIES = 500;
7
+ let mem = {};
8
+ export function loadCache() {
9
+ if (!existsSync(CACHE_FILE))
10
+ return;
11
+ try {
12
+ mem = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
13
+ }
14
+ catch { }
15
+ }
16
+ function persistCache() {
17
+ try {
18
+ writeFileSync(CACHE_FILE, JSON.stringify(mem));
19
+ }
20
+ catch { }
21
+ }
22
+ /** Normalize a natural language query for cache lookup.
23
+ * Keeps . / - _ which are meaningful in file paths and shell context. */
24
+ export function normalizeNl(nl) {
25
+ return nl
26
+ .toLowerCase()
27
+ .trim()
28
+ .replace(/[^a-z0-9\s.\/_-]/g, "") // keep meaningful shell chars
29
+ .replace(/\s+/g, " ");
30
+ }
31
+ export function cacheGet(nl) {
32
+ return mem[normalizeNl(nl)] ?? null;
33
+ }
34
+ export function cacheSet(nl, command) {
35
+ const key = normalizeNl(nl);
36
+ // evict oldest if full
37
+ const keys = Object.keys(mem);
38
+ if (keys.length >= MAX_ENTRIES)
39
+ delete mem[keys[0]];
40
+ mem[key] = command;
41
+ persistCache();
42
+ }