scalix 1.1.0 → 1.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/commands.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
+ import { existsSync, readFileSync } from 'node:fs';
2
3
  import path from 'node:path';
4
+ import { execSync } from 'node:child_process';
3
5
  import { logger } from './logger.js';
4
6
  import { PROJECT_LOOKUP_DIRS, PROJECT_CONFIG_DIR } from './constants.js';
5
7
  const COMMAND_DIRS = PROJECT_LOOKUP_DIRS.map(d => `${d}/commands`);
@@ -16,6 +18,9 @@ function parseFrontmatter(content) {
16
18
  }
17
19
  return { meta, body: match[2] };
18
20
  }
21
+ function parseAllowedTools(spec) {
22
+ return spec.split(',').map(s => s.trim()).filter(Boolean);
23
+ }
19
24
  export async function loadCustomCommands(cwd = process.cwd()) {
20
25
  const commands = [];
21
26
  for (const dir of COMMAND_DIRS) {
@@ -35,11 +40,17 @@ export async function loadCustomCommands(cwd = process.cwd()) {
35
40
  const raw = await fs.readFile(filePath, 'utf-8');
36
41
  const { meta, body } = parseFrontmatter(raw);
37
42
  const name = entry.name.replace(/\.md$/, '');
43
+ const fm = meta;
38
44
  commands.push({
39
45
  name,
40
- description: meta.description || `Custom command: ${name}`,
46
+ description: fm.description || `Custom command: ${name}`,
41
47
  template: body.trim(),
42
48
  source: filePath,
49
+ frontmatter: fm,
50
+ allowedTools: fm['allowed-tools'] ? parseAllowedTools(fm['allowed-tools']) : undefined,
51
+ model: fm.model || undefined,
52
+ argumentHint: fm['argument-hint'] || undefined,
53
+ disableModelInvocation: fm['disable-model-invocation'] === 'true',
43
54
  });
44
55
  }
45
56
  catch (err) {
@@ -49,15 +60,52 @@ export async function loadCustomCommands(cwd = process.cwd()) {
49
60
  }
50
61
  return commands;
51
62
  }
63
+ function expandFileReferences(text, cwd) {
64
+ return text.replace(/@([\w./-]+\.\w+)/g, (_match, filePath) => {
65
+ const resolved = path.resolve(cwd, filePath);
66
+ if (!existsSync(resolved))
67
+ return `@${filePath} (file not found)`;
68
+ try {
69
+ const content = readFileSync(resolved, 'utf-8');
70
+ return `\n<file path="${filePath}">\n${content}\n</file>\n`;
71
+ }
72
+ catch {
73
+ return `@${filePath} (read error)`;
74
+ }
75
+ });
76
+ }
77
+ function expandInlineBash(text) {
78
+ return text.replace(/!`([^`]+)`/g, (_match, command) => {
79
+ try {
80
+ return execSync(command, { encoding: 'utf-8', timeout: 10_000 }).trim();
81
+ }
82
+ catch (err) {
83
+ return `(command failed: ${err.message})`;
84
+ }
85
+ });
86
+ }
87
+ function expandPositionalArgs(text, args) {
88
+ const parts = args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
89
+ const cleaned = parts.map(p => p.replace(/^["']|["']$/g, ''));
90
+ let result = text;
91
+ for (let i = 0; i < 9; i++) {
92
+ const placeholder = `$${i + 1}`;
93
+ result = result.split(placeholder).join(cleaned[i] || '');
94
+ }
95
+ return result;
96
+ }
52
97
  export function expandCommand(command, args) {
53
98
  const safeArgs = args
54
99
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '')
55
100
  .replace(/\$/g, '\\$')
56
101
  .replace(/`/g, '\\`');
57
102
  let prompt = command.template;
103
+ prompt = expandPositionalArgs(prompt, args);
58
104
  prompt = prompt.replace(/\$ARGS/g, safeArgs);
59
105
  prompt = prompt.replace(/\$CWD/g, process.cwd());
60
106
  prompt = prompt.replace(/\$DATE/g, new Date().toISOString().split('T')[0]);
107
+ prompt = expandFileReferences(prompt, process.cwd());
108
+ prompt = expandInlineBash(prompt);
61
109
  return prompt;
62
110
  }
63
111
  export function commandsSummary(commands) {
@@ -65,7 +113,10 @@ export function commandsSummary(commands) {
65
113
  return `No custom commands. Add .md files to ${PROJECT_CONFIG_DIR}/commands/`;
66
114
  const lines = ['Custom commands:'];
67
115
  for (const cmd of commands) {
68
- lines.push(` /${cmd.name} — ${cmd.description}`);
116
+ const hint = cmd.argumentHint ? ` <${cmd.argumentHint}>` : '';
117
+ const model = cmd.model ? ` [model: ${cmd.model}]` : '';
118
+ const tools = cmd.allowedTools ? ` [tools: ${cmd.allowedTools.length}]` : '';
119
+ lines.push(` /${cmd.name}${hint} — ${cmd.description}${model}${tools}`);
69
120
  }
70
121
  return lines.join('\n');
71
122
  }
package/dist/constants.js CHANGED
@@ -239,10 +239,10 @@ export const PROJECT_INDEX_DEFAULTS = {
239
239
  symbolFileLimit: 1_000,
240
240
  statusDisplayLimit: 50,
241
241
  };
242
- // ─── Git Identity (GitHub App bot) ───
242
+ // ─── Git Identity ───
243
243
  export const GIT_BOT_IDENTITY = {
244
- name: 'scalix-coder[bot]',
245
- email: '280249884+scalix-coder[bot]@users.noreply.github.com',
244
+ name: 'Scalix Coder',
245
+ email: '283104927+scalix-coder@users.noreply.github.com',
246
246
  };
247
247
  // ─── Git Context Defaults ───
248
248
  export const GIT_CONTEXT_DEFAULTS = {
package/dist/headless.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import readline from 'node:readline';
2
2
  import fs from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  import { execSync } from 'node:child_process';
4
5
  import { createAgent } from './agent.js';
5
6
  import { defaultTools } from './tools.js';
@@ -39,6 +40,8 @@ export function parseArgs() {
39
40
  let format;
40
41
  let permissionMode;
41
42
  let outputSchema;
43
+ let worktree = false;
44
+ let worktreeName;
42
45
  for (let i = 0; i < args.length; i++) {
43
46
  if (args[i] === 'run' && i + 1 < args.length) {
44
47
  mode = 'run';
@@ -72,6 +75,13 @@ export function parseArgs() {
72
75
  else if (args[i] === '--ipc') {
73
76
  mode = 'ipc';
74
77
  }
78
+ else if (args[i] === '--worktree' || args[i] === '-w') {
79
+ worktree = true;
80
+ if (i + 1 < args.length && !args[i + 1].startsWith('-') && args[i + 1] !== 'run') {
81
+ worktreeName = args[i + 1];
82
+ i++;
83
+ }
84
+ }
75
85
  else if (args[i] === '--help' || args[i] === '-h') {
76
86
  console.log(`
77
87
  Scalix - Headless Mode
@@ -91,6 +101,7 @@ Options:
91
101
  --format <fmt> Output format: text (default), json, json-schema, patch, markdown
92
102
  --output-format Alias for --format
93
103
  --output-schema Path to JSON schema file (implies --format json-schema)
104
+ --worktree, -w Run in an isolated git worktree (optional: --worktree <name>)
94
105
  --help, -h Show this help
95
106
  `);
96
107
  process.exit(0);
@@ -99,7 +110,7 @@ Options:
99
110
  if (!process.stdin.isTTY && mode === 'interactive') {
100
111
  mode = 'pipe';
101
112
  }
102
- return { mode, prompt, output, format, permissionMode, outputSchema };
113
+ return { mode, prompt, output, format, permissionMode, outputSchema, worktree, worktreeName };
103
114
  }
104
115
  async function buildEnhancedInstructions() {
105
116
  let base = DEFAULT_INSTRUCTIONS;
@@ -713,10 +724,39 @@ async function runInteractive() {
713
724
  }
714
725
  async function main() {
715
726
  setSandboxRoot(process.cwd());
716
- const { mode, prompt, output, format, permissionMode: cliPermissionMode, outputSchema } = parseArgs();
727
+ const { mode, prompt, output, format, permissionMode: cliPermissionMode, outputSchema, worktree, worktreeName } = parseArgs();
717
728
  if (cliPermissionMode) {
718
729
  process.env.SCALIX_PERMISSION_MODE = cliPermissionMode;
719
730
  }
731
+ // Set up worktree isolation if requested
732
+ let worktreeInfo = null;
733
+ if (worktree) {
734
+ const { createWorktree, removeWorktree } = await import('./worktree.js');
735
+ const name = worktreeName || `session-${Date.now().toString(36)}`;
736
+ try {
737
+ worktreeInfo = await createWorktree(name, process.cwd());
738
+ process.chdir(worktreeInfo.path);
739
+ setSandboxRoot(worktreeInfo.path);
740
+ console.log(`Working in isolated worktree: ${worktreeInfo.branch} → ${worktreeInfo.path}`);
741
+ }
742
+ catch (err) {
743
+ console.error(`Failed to create worktree: ${err.message}`);
744
+ process.exit(1);
745
+ }
746
+ const cleanup = async () => {
747
+ if (worktreeInfo) {
748
+ try {
749
+ const origCwd = process.env.SCALIX_ORIGINAL_CWD || process.cwd();
750
+ process.chdir(path.dirname(worktreeInfo.path));
751
+ await removeWorktree(worktreeInfo, path.dirname(path.dirname(worktreeInfo.path)));
752
+ }
753
+ catch { }
754
+ }
755
+ };
756
+ process.on('exit', () => { cleanup().catch(() => { }); });
757
+ process.on('SIGINT', () => { cleanup().then(() => process.exit(0)).catch(() => process.exit(1)); });
758
+ process.env.SCALIX_ORIGINAL_CWD = process.cwd();
759
+ }
720
760
  switch (mode) {
721
761
  case 'run':
722
762
  if (!prompt) {
package/dist/hooks.js CHANGED
@@ -29,26 +29,112 @@ function matchesHook(hook, toolName) {
29
29
  return true;
30
30
  if (!toolName)
31
31
  return false;
32
- if (hook.match === toolName)
32
+ const patterns = hook.match.split('|').map(p => p.trim()).filter(Boolean);
33
+ return patterns.some(pattern => matchPattern(pattern, toolName));
34
+ }
35
+ function matchPattern(pattern, value) {
36
+ if (pattern === value)
37
+ return true;
38
+ if (pattern === '*')
33
39
  return true;
34
- if (hook.match.endsWith('*')) {
35
- return toolName.startsWith(hook.match.slice(0, -1));
40
+ if (pattern.endsWith('*') && !pattern.startsWith('/')) {
41
+ return value.startsWith(pattern.slice(0, -1));
42
+ }
43
+ if (pattern.startsWith('*') && !pattern.endsWith('*')) {
44
+ return value.endsWith(pattern.slice(1));
45
+ }
46
+ if (pattern.startsWith('/') && pattern.endsWith('/')) {
47
+ try {
48
+ return new RegExp(pattern.slice(1, -1)).test(value);
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ if (pattern.includes('*')) {
55
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
56
+ try {
57
+ return new RegExp(`^${regex}$`).test(value);
58
+ }
59
+ catch {
60
+ return false;
61
+ }
36
62
  }
37
63
  return false;
38
64
  }
39
- export async function executeHook(hook, env) {
65
+ function buildHookInput(event, context) {
66
+ const input = {
67
+ event,
68
+ timestamp: new Date().toISOString(),
69
+ };
70
+ if (context.toolName)
71
+ input.tool_name = context.toolName;
72
+ if (context.toolArgs) {
73
+ try {
74
+ input.tool_args = JSON.parse(context.toolArgs);
75
+ }
76
+ catch {
77
+ input.tool_args = context.toolArgs;
78
+ }
79
+ }
80
+ if (context.toolResult !== undefined)
81
+ input.tool_result = context.toolResult;
82
+ if (context.error)
83
+ input.error = context.error;
84
+ if (context.prompt)
85
+ input.prompt = context.prompt.slice(0, 4096);
86
+ if (context.response)
87
+ input.response = context.response.slice(0, 4096);
88
+ if (context.notification)
89
+ input.notification = context.notification;
90
+ return JSON.stringify(input);
91
+ }
92
+ function parseHookOutput(stdout, exitCode) {
93
+ const trimmed = stdout.trim();
94
+ if (trimmed.startsWith('{')) {
95
+ try {
96
+ const parsed = JSON.parse(trimmed);
97
+ return {
98
+ allowed: exitCode === 0 || exitCode === undefined,
99
+ output: parsed.output ?? trimmed,
100
+ message: parsed.message,
101
+ data: parsed,
102
+ };
103
+ }
104
+ catch { }
105
+ }
106
+ const allowed = exitCode === 0 || exitCode === undefined;
107
+ return {
108
+ allowed,
109
+ output: trimmed || undefined,
110
+ message: exitCode === 2 ? trimmed : undefined,
111
+ };
112
+ }
113
+ export async function executeHook(hook, env, stdinData) {
40
114
  const timeout = hook.timeout ?? HOOK_DEFAULTS.timeoutMs;
41
115
  const hookEnv = { ...process.env, ...env };
42
116
  const ac = new AbortController();
43
117
  const hardKill = setTimeout(() => ac.abort(), timeout + 5000);
44
118
  try {
45
- const { stdout, stderr } = await execAsync(hook.command, {
119
+ const child = exec(hook.command, {
46
120
  timeout,
47
121
  env: hookEnv,
48
122
  maxBuffer: HOOK_DEFAULTS.maxBuffer,
49
123
  signal: ac.signal,
50
124
  });
51
- return { allowed: true, output: (stdout || stderr).trim() || undefined };
125
+ if (stdinData && child.stdin) {
126
+ child.stdin.write(stdinData);
127
+ child.stdin.end();
128
+ }
129
+ const result = await new Promise((resolve, reject) => {
130
+ let stdout = '';
131
+ let stderr = '';
132
+ child.stdout?.on('data', (d) => { stdout += d; });
133
+ child.stderr?.on('data', (d) => { stderr += d; });
134
+ child.on('error', reject);
135
+ child.on('close', (code) => resolve({ stdout, stderr, code: code ?? 0 }));
136
+ });
137
+ return parseHookOutput(result.stdout, result.code);
52
138
  }
53
139
  catch (err) {
54
140
  if (err.killed || err.code === 'ETIMEDOUT' || err.name === 'AbortError') {
@@ -65,7 +151,25 @@ export async function executeHook(hook, env) {
65
151
  }
66
152
  }
67
153
  export async function runHooks(hooks, event, context) {
68
- const matching = hooks.filter((h) => h.event === event && matchesHook(h, context.toolName));
154
+ const matching = hooks.filter((h) => {
155
+ if (h.event !== event)
156
+ return false;
157
+ if (!matchesHook(h, context.toolName))
158
+ return false;
159
+ if (h.type === 'prompt')
160
+ return false;
161
+ if (h.if) {
162
+ try {
163
+ const conditionMet = evaluateCondition(h.if, context);
164
+ if (!conditionMet)
165
+ return false;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ return true;
172
+ });
69
173
  if (matching.length === 0)
70
174
  return [];
71
175
  const env = {};
@@ -77,10 +181,15 @@ export async function runHooks(hooks, event, context) {
77
181
  env.SCALIX_ERROR = context.error;
78
182
  if (context.prompt)
79
183
  env.SCALIX_PROMPT = context.prompt.slice(0, 4096).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
184
+ if (context.response)
185
+ env.SCALIX_RESPONSE = context.response.slice(0, 4096);
186
+ if (context.notification)
187
+ env.SCALIX_NOTIFICATION = context.notification;
80
188
  env.SCALIX_EVENT = event;
189
+ const stdinData = buildHookInput(event, context);
81
190
  const results = [];
82
191
  for (const hook of matching) {
83
- const result = await executeHook(hook, env);
192
+ const result = await executeHook(hook, env, stdinData);
84
193
  results.push(result);
85
194
  logger.debug('Hook executed', { event, command: hook.command, allowed: result.allowed });
86
195
  if (!result.allowed && event === 'PreToolUse')
@@ -88,13 +197,36 @@ export async function runHooks(hooks, event, context) {
88
197
  }
89
198
  return results;
90
199
  }
200
+ function evaluateCondition(condition, context) {
201
+ if (condition.startsWith('tool_name=')) {
202
+ const expected = condition.slice('tool_name='.length);
203
+ return matchPattern(expected, context.toolName || '');
204
+ }
205
+ if (condition.startsWith('tool_args~')) {
206
+ const pattern = condition.slice('tool_args~'.length);
207
+ try {
208
+ return new RegExp(pattern).test(context.toolArgs || '');
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ return true;
215
+ }
216
+ export function getPromptHooks(hooks, event, toolName) {
217
+ return hooks.filter(h => h.event === event &&
218
+ h.type === 'prompt' &&
219
+ matchesHook(h, toolName));
220
+ }
91
221
  export function hooksSummary(hooks) {
92
222
  if (hooks.length === 0)
93
223
  return 'No hooks configured.';
94
224
  const lines = ['Configured hooks:'];
95
225
  for (const h of hooks) {
96
226
  const match = h.match ? ` (match: ${h.match})` : '';
97
- lines.push(` ${h.event}${match}: ${h.command}`);
227
+ const type = h.type === 'prompt' ? ' [prompt]' : '';
228
+ const cond = h.if ? ` [if: ${h.if}]` : '';
229
+ lines.push(` ${h.event}${match}${type}${cond}: ${h.command}`);
98
230
  }
99
231
  return lines.join('\n');
100
232
  }
@@ -0,0 +1,109 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const exec = promisify(execFile);
4
+ const SERVICE = 'scalix-coder';
5
+ const ACCOUNT = 'api-key';
6
+ async function isMacOS() {
7
+ return process.platform === 'darwin';
8
+ }
9
+ async function isLinuxWithSecretTool() {
10
+ if (process.platform !== 'linux')
11
+ return false;
12
+ try {
13
+ await exec('which', ['secret-tool']);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export async function getApiKeyFromKeychain() {
21
+ try {
22
+ if (await isMacOS()) {
23
+ const { stdout } = await exec('security', [
24
+ 'find-generic-password',
25
+ '-s', SERVICE,
26
+ '-a', ACCOUNT,
27
+ '-w',
28
+ ]);
29
+ return stdout.trim() || null;
30
+ }
31
+ if (await isLinuxWithSecretTool()) {
32
+ const { stdout } = await exec('secret-tool', [
33
+ 'lookup', 'service', SERVICE, 'account', ACCOUNT,
34
+ ]);
35
+ return stdout.trim() || null;
36
+ }
37
+ }
38
+ catch {
39
+ // keychain item not found or command failed
40
+ }
41
+ return null;
42
+ }
43
+ export async function setApiKeyInKeychain(apiKey) {
44
+ try {
45
+ if (await isMacOS()) {
46
+ // Delete existing entry first (ignore errors if it doesn't exist)
47
+ try {
48
+ await exec('security', [
49
+ 'delete-generic-password',
50
+ '-s', SERVICE,
51
+ '-a', ACCOUNT,
52
+ ]);
53
+ }
54
+ catch {
55
+ // entry didn't exist
56
+ }
57
+ await exec('security', [
58
+ 'add-generic-password',
59
+ '-s', SERVICE,
60
+ '-a', ACCOUNT,
61
+ '-w', apiKey,
62
+ '-U',
63
+ ]);
64
+ return true;
65
+ }
66
+ if (await isLinuxWithSecretTool()) {
67
+ const child = exec('secret-tool', [
68
+ 'store',
69
+ '--label', 'Scalix Coder API Key',
70
+ 'service', SERVICE,
71
+ 'account', ACCOUNT,
72
+ ]);
73
+ // secret-tool reads the secret from stdin
74
+ child.child?.stdin?.write(apiKey);
75
+ child.child?.stdin?.end();
76
+ await child;
77
+ return true;
78
+ }
79
+ }
80
+ catch {
81
+ // keychain operation failed
82
+ }
83
+ return false;
84
+ }
85
+ export async function deleteApiKeyFromKeychain() {
86
+ try {
87
+ if (await isMacOS()) {
88
+ await exec('security', [
89
+ 'delete-generic-password',
90
+ '-s', SERVICE,
91
+ '-a', ACCOUNT,
92
+ ]);
93
+ return true;
94
+ }
95
+ if (await isLinuxWithSecretTool()) {
96
+ await exec('secret-tool', [
97
+ 'clear', 'service', SERVICE, 'account', ACCOUNT,
98
+ ]);
99
+ return true;
100
+ }
101
+ }
102
+ catch {
103
+ // entry didn't exist or command failed
104
+ }
105
+ return false;
106
+ }
107
+ export async function isKeychainAvailable() {
108
+ return await isMacOS() || await isLinuxWithSecretTool();
109
+ }
package/dist/markdown.js CHANGED
@@ -1,13 +1,42 @@
1
1
  import chalk from 'chalk';
2
2
  import { Marked } from 'marked';
3
3
  import { markedTerminal } from 'marked-terminal';
4
+ let highlightFn = null;
5
+ let highlightLoaded = false;
6
+ function getHighlighter() {
7
+ if (highlightLoaded)
8
+ return highlightFn;
9
+ highlightLoaded = true;
10
+ try {
11
+ // cli-highlight is a dependency — provides 100+ language highlighting
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ const mod = require('cli-highlight');
14
+ highlightFn = mod.highlight;
15
+ }
16
+ catch {
17
+ highlightFn = null;
18
+ }
19
+ return highlightFn;
20
+ }
21
+ function highlightCode(code, language) {
22
+ const hl = getHighlighter();
23
+ if (!hl)
24
+ return chalk.yellow(code);
25
+ try {
26
+ return hl(code, { language: language || undefined });
27
+ }
28
+ catch {
29
+ // Unknown language or highlight error — fall back to yellow
30
+ return chalk.yellow(code);
31
+ }
32
+ }
4
33
  function createMarked(width) {
5
34
  const m = new Marked();
6
35
  const mt = markedTerminal({
7
36
  strong: chalk.bold,
8
37
  em: chalk.italic,
9
38
  codespan: chalk.cyan,
10
- code: chalk.yellow,
39
+ code: highlightCode,
11
40
  heading: chalk.green.bold,
12
41
  firstHeading: chalk.magenta.bold,
13
42
  hr: chalk.dim,
@@ -25,15 +54,29 @@ function createMarked(width) {
25
54
  }
26
55
  let cachedWidth = 0;
27
56
  let cachedMarked = null;
57
+ // Content-hash render cache to avoid re-rendering identical content
58
+ const renderCache = new Map();
59
+ const MAX_CACHE_SIZE = 128;
28
60
  export function renderMarkdown(text, cols) {
29
61
  const width = Math.max((cols ?? process.stdout.columns ?? 100) - 6, 40);
62
+ const cacheKey = `${width}:${text}`;
63
+ const cached = renderCache.get(cacheKey);
64
+ if (cached)
65
+ return cached;
30
66
  if (!cachedMarked || width !== cachedWidth) {
31
67
  cachedWidth = width;
32
68
  cachedMarked = createMarked(width);
33
69
  }
34
70
  try {
35
- const result = cachedMarked.parse(text);
36
- return result.replace(/\n{3,}/g, '\n\n').trim();
71
+ const result = cachedMarked.parse(text).replace(/\n{3,}/g, '\n\n').trim();
72
+ // Evict oldest entries when cache is full
73
+ if (renderCache.size >= MAX_CACHE_SIZE) {
74
+ const firstKey = renderCache.keys().next().value;
75
+ if (firstKey !== undefined)
76
+ renderCache.delete(firstKey);
77
+ }
78
+ renderCache.set(cacheKey, result);
79
+ return result;
37
80
  }
38
81
  catch {
39
82
  return text;