@wangzhizhi/remi 0.0.1-alpha
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/README.md +9 -0
- package/dist/doctor.js +108 -0
- package/dist/git.js +41 -0
- package/dist/help.js +27 -0
- package/dist/i18n.js +422 -0
- package/dist/index.js +97 -0
- package/dist/initPrompt.js +17 -0
- package/dist/model.js +116 -0
- package/dist/modelSelection.js +34 -0
- package/dist/permissionDisplay.js +46 -0
- package/dist/permissions.js +206 -0
- package/dist/repl.js +346 -0
- package/dist/resume.js +3 -0
- package/dist/setup.js +62 -0
- package/dist/statusline.js +59 -0
- package/dist/style.js +48 -0
- package/dist/syntaxTheme.js +39 -0
- package/dist/tui/RemiApp.js +1756 -0
- package/dist/tui/commands.js +427 -0
- package/dist/tui/index.js +42 -0
- package/dist/tui/renderers/Header.js +28 -0
- package/dist/tui/renderers/MessageList.js +1176 -0
- package/dist/tui/renderers/PromptBox.js +118 -0
- package/dist/tui/renderers/StatusLine.js +124 -0
- package/dist/tui/renderers/WorkingIndicator.js +70 -0
- package/dist/tui/slashCommandHighlight.js +8 -0
- package/dist/tui/theme.js +13 -0
- package/dist/tui/types.js +1 -0
- package/dist/usage.js +66 -0
- package/dist/version.js +5 -0
- package/node_modules/@remi/compact/dist/index.js +389 -0
- package/node_modules/@remi/compact/package.json +8 -0
- package/node_modules/@remi/config/dist/index.js +426 -0
- package/node_modules/@remi/config/package.json +8 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +344 -0
- package/node_modules/@remi/core/dist/directoryOverview.js +359 -0
- package/node_modules/@remi/core/dist/index.js +2843 -0
- package/node_modules/@remi/core/dist/projectInstructions.js +123 -0
- package/node_modules/@remi/core/dist/responseStyles.js +98 -0
- package/node_modules/@remi/core/package.json +8 -0
- package/node_modules/@remi/llm/dist/index.js +804 -0
- package/node_modules/@remi/llm/package.json +8 -0
- package/node_modules/@remi/memory/dist/index.js +312 -0
- package/node_modules/@remi/memory/package.json +8 -0
- package/node_modules/@remi/permissions/dist/index.js +90 -0
- package/node_modules/@remi/permissions/package.json +8 -0
- package/node_modules/@remi/sessions/dist/index.js +370 -0
- package/node_modules/@remi/sessions/package.json +8 -0
- package/node_modules/@remi/skills/dist/index.js +273 -0
- package/node_modules/@remi/skills/package.json +8 -0
- package/node_modules/@remi/terminal-markdown/dist/index.js +1412 -0
- package/node_modules/@remi/terminal-markdown/package.json +8 -0
- package/node_modules/@remi/tools/dist/index.js +3875 -0
- package/node_modules/@remi/tools/package.json +8 -0
- package/package.json +48 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
const overviewIntentPattern = /(看|看看|检查|瞅|浏览|了解|列|列出|有什么|包含|目录|文件夹|folder|directory|contents|what.*in)/i;
|
|
2
|
+
const directoryWords = ['文件夹', '目录', 'folder', 'directory'];
|
|
3
|
+
const desktopBookPattern = /(?:桌面|desktop).*book|book.*(?:文件夹|目录|folder|directory)/i;
|
|
4
|
+
export async function buildDirectoryOverviewPrelude(options) {
|
|
5
|
+
const targetPath = detectDirectoryOverviewTarget(options.input, options.env);
|
|
6
|
+
if (!targetPath) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const toolCalls = [];
|
|
10
|
+
const run = async (toolName, input) => {
|
|
11
|
+
const call = {
|
|
12
|
+
id: `prelude-${toolCalls.length + 1}`,
|
|
13
|
+
toolName,
|
|
14
|
+
input,
|
|
15
|
+
};
|
|
16
|
+
const outcome = await options.executor.execute({
|
|
17
|
+
call,
|
|
18
|
+
context: {
|
|
19
|
+
cwd: options.cwd,
|
|
20
|
+
sessionId: options.sessionId,
|
|
21
|
+
maxOutputBytes: 128 * 1024,
|
|
22
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
23
|
+
permissionMode: 'readonly',
|
|
24
|
+
allowedReadRoots: allowedReadRoots(options.cwd, options.env),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
toolCalls.push({ call, result: outcome.result });
|
|
28
|
+
return outcome.result;
|
|
29
|
+
};
|
|
30
|
+
const topLevel = await run('list_files', { path: targetPath, includeHidden: true, maxEntries: 200 });
|
|
31
|
+
const entries = parseEntries(topLevel);
|
|
32
|
+
const guide = selectGuideFile(entries);
|
|
33
|
+
if (guide) {
|
|
34
|
+
await run('read_file', { path: guide, maxBytes: 12_000 });
|
|
35
|
+
}
|
|
36
|
+
for (const directory of selectImportantDirectories(entries)) {
|
|
37
|
+
await run('list_files', { path: directory, maxEntries: 120 });
|
|
38
|
+
}
|
|
39
|
+
const contentDirectory = selectContentDirectory(entries);
|
|
40
|
+
if (contentDirectory) {
|
|
41
|
+
await run('glob', { path: contentDirectory, pattern: '**/*.md', maxResults: 300 });
|
|
42
|
+
}
|
|
43
|
+
const summaryFile = selectSummaryFile(entries);
|
|
44
|
+
if (summaryFile) {
|
|
45
|
+
await run('read_file', { path: summaryFile, maxBytes: 16_000 });
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
targetPath,
|
|
49
|
+
toolCalls,
|
|
50
|
+
messages: [formatDirectoryOverviewContext(targetPath, toolCalls)],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function detectDirectoryOverviewTarget(input, env) {
|
|
54
|
+
const normalized = input.trim();
|
|
55
|
+
if (!overviewIntentPattern.test(normalized)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (desktopBookPattern.test(normalized) && env.HOME) {
|
|
59
|
+
return `${env.HOME}/Desktop/book`;
|
|
60
|
+
}
|
|
61
|
+
const absoluteMatch = normalized.match(/(\/[^\s,。!?!?]+(?:\/[^\s,。!?!?]+)*)/);
|
|
62
|
+
if (absoluteMatch?.[1]) {
|
|
63
|
+
return absoluteMatch[1];
|
|
64
|
+
}
|
|
65
|
+
const quoted = normalized.match(/["'`“”‘’]([^"'`“”‘’]+)["'`“”‘’]/)?.[1];
|
|
66
|
+
if (quoted && looksLikeDirectoryReference(normalized, quoted)) {
|
|
67
|
+
return quoted;
|
|
68
|
+
}
|
|
69
|
+
const relative = normalized.match(/(?:看看|看|检查|浏览|列出|list|inspect)\s+([A-Za-z0-9._~/-]+)/i)?.[1];
|
|
70
|
+
if (relative) {
|
|
71
|
+
return relative;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function looksLikeDirectoryReference(input, target) {
|
|
76
|
+
return directoryWords.some(word => input.toLowerCase().includes(word)) || target.includes('/') || target.includes('\\');
|
|
77
|
+
}
|
|
78
|
+
function formatDirectoryOverviewContext(targetPath, toolCalls) {
|
|
79
|
+
const sections = [
|
|
80
|
+
'## Directory Overview Prelude',
|
|
81
|
+
'',
|
|
82
|
+
`Target directory: ${targetPath}`,
|
|
83
|
+
'',
|
|
84
|
+
'Remi already performed a bounded read-only overview before answering. Use this context to produce a project-level explanation, not just a top-level listing.',
|
|
85
|
+
'',
|
|
86
|
+
'Answer rules:',
|
|
87
|
+
'- Separate confirmed facts from filename-based inference.',
|
|
88
|
+
'- Mention counts, latest-looking files, guide files, summaries, indexes, archives, and hidden config when present.',
|
|
89
|
+
'- If a guide or summary file was read, use it to explain what the project is for.',
|
|
90
|
+
'- Do not claim certainty about contents you did not inspect.',
|
|
91
|
+
'- Prefer a compact tree view first when the user asks what a directory contains, then add a short "core files" explanation.',
|
|
92
|
+
'',
|
|
93
|
+
formatDirectoryTreeFacts(targetPath, toolCalls),
|
|
94
|
+
'',
|
|
95
|
+
...toolCalls.flatMap(({ call, result }) => [
|
|
96
|
+
`### ${formatToolCallTitle(call)}`,
|
|
97
|
+
result.ok ? compactToolOutput(result.output) : `Error: ${result.error.code}: ${result.error.message}`,
|
|
98
|
+
'',
|
|
99
|
+
]),
|
|
100
|
+
];
|
|
101
|
+
return { role: 'system', content: sections.join('\n') };
|
|
102
|
+
}
|
|
103
|
+
function formatDirectoryTreeFacts(targetPath, toolCalls) {
|
|
104
|
+
const listedDirectories = parseListedDirectories(toolCalls);
|
|
105
|
+
const topLevel = listedDirectories[0];
|
|
106
|
+
if (!topLevel) {
|
|
107
|
+
return '### Suggested tree view\nNo directory listing facts are available.';
|
|
108
|
+
}
|
|
109
|
+
const rootPath = topLevel.path;
|
|
110
|
+
const rootName = basename(rootPath === '.' ? targetPath : rootPath);
|
|
111
|
+
const childListings = new Map(listedDirectories.slice(1).map(directory => [directory.path, directory]));
|
|
112
|
+
const globMatches = parseGlobMatches(toolCalls);
|
|
113
|
+
const contentDirectories = new Map();
|
|
114
|
+
for (const match of globMatches) {
|
|
115
|
+
const parent = dirname(match);
|
|
116
|
+
contentDirectories.set(parent, [...(contentDirectories.get(parent) ?? []), match]);
|
|
117
|
+
}
|
|
118
|
+
const lines = ['### Suggested tree view', '', 'Use this tree as factual scaffolding for the answer:', '', `${rootName}/`];
|
|
119
|
+
const entries = orderEntriesForTree(topLevel.entries);
|
|
120
|
+
entries.forEach((entry, index) => {
|
|
121
|
+
const isLast = index === entries.length - 1;
|
|
122
|
+
appendTreeEntry(lines, entry, {
|
|
123
|
+
isLast,
|
|
124
|
+
prefix: '',
|
|
125
|
+
rootPath,
|
|
126
|
+
childListings,
|
|
127
|
+
contentDirectories,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
const fileEntries = entries.filter(entry => entry.type === 'file');
|
|
131
|
+
if (fileEntries.length > 0) {
|
|
132
|
+
lines.push('', 'Core top-level files:');
|
|
133
|
+
for (const file of fileEntries.slice(0, 8)) {
|
|
134
|
+
lines.push(`- ${basename(file.path)}${file.sizeBytes !== undefined ? ` (${formatBytes(file.sizeBytes)})` : ''}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
function parseListedDirectories(toolCalls) {
|
|
140
|
+
return toolCalls
|
|
141
|
+
.flatMap(({ call, result }) => {
|
|
142
|
+
if (call.toolName !== 'list_files' || !result.ok) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const parsed = parseToolOutput(result.output);
|
|
146
|
+
const path = typeof parsed?.['path'] === 'string' ? parsed['path'] : '';
|
|
147
|
+
const entries = Array.isArray(parsed?.['entries']) ? parsed['entries'].filter(isDirectoryEntry) : [];
|
|
148
|
+
return [{ path, entries }];
|
|
149
|
+
})
|
|
150
|
+
.filter(directory => directory.path && directory.entries.length > 0);
|
|
151
|
+
}
|
|
152
|
+
function parseGlobMatches(toolCalls) {
|
|
153
|
+
return toolCalls
|
|
154
|
+
.flatMap(({ call, result }) => {
|
|
155
|
+
if (call.toolName !== 'glob' || !result.ok) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const parsed = parseToolOutput(result.output);
|
|
159
|
+
return Array.isArray(parsed?.['matches']) ? parsed['matches'].filter((item) => typeof item === 'string') : [];
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function appendTreeEntry(lines, entry, options) {
|
|
163
|
+
const connector = options.isLast ? '└── ' : '├── ';
|
|
164
|
+
const childPrefix = `${options.prefix}${options.isLast ? ' ' : '│ '}`;
|
|
165
|
+
const name = `${basename(entry.path)}${entry.type === 'directory' ? '/' : ''}`;
|
|
166
|
+
const annotation = annotationForEntry(entry, options.contentDirectories.get(entry.path));
|
|
167
|
+
lines.push(`${options.prefix}${connector}${name}${annotation ? ` # ${annotation}` : ''}`);
|
|
168
|
+
if (entry.type !== 'directory') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const contentMatches = options.contentDirectories.get(entry.path);
|
|
172
|
+
if (contentMatches && contentMatches.length > 0) {
|
|
173
|
+
appendContentSamples(lines, childPrefix, contentMatches);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const childListing = options.childListings.get(entry.path);
|
|
177
|
+
if (!childListing) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const children = orderEntriesForTree(childListing.entries).slice(0, 8);
|
|
181
|
+
children.forEach((child, index) => {
|
|
182
|
+
appendTreeEntry(lines, child, {
|
|
183
|
+
...options,
|
|
184
|
+
isLast: index === children.length - 1,
|
|
185
|
+
prefix: childPrefix,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function appendContentSamples(lines, prefix, matches) {
|
|
190
|
+
const sorted = [...matches].sort((left, right) => left.localeCompare(right));
|
|
191
|
+
const samples = sorted.length <= 4 ? sorted : [sorted[0], sorted[1], '...', sorted[sorted.length - 1]].filter((item) => Boolean(item));
|
|
192
|
+
samples.forEach((sample, index) => {
|
|
193
|
+
const isLast = index === samples.length - 1;
|
|
194
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
195
|
+
lines.push(`${prefix}${connector}${sample === '...' ? '...' : basename(sample)}`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function annotationForEntry(entry, contentMatches) {
|
|
199
|
+
if (contentMatches && contentMatches.length > 0) {
|
|
200
|
+
const markdownCount = contentMatches.filter(match => match.toLowerCase().endsWith('.md')).length;
|
|
201
|
+
return markdownCount > 0 ? `${markdownCount} markdown files` : `${contentMatches.length} matched files`;
|
|
202
|
+
}
|
|
203
|
+
if (entry.type === 'file' && entry.sizeBytes !== undefined) {
|
|
204
|
+
return formatBytes(entry.sizeBytes);
|
|
205
|
+
}
|
|
206
|
+
const lowerName = basename(entry.path).toLowerCase();
|
|
207
|
+
if (lowerName === '.claude') {
|
|
208
|
+
return 'local Claude configuration';
|
|
209
|
+
}
|
|
210
|
+
if (lowerName === 'old' || lowerName === 'archive') {
|
|
211
|
+
return 'archive or old version';
|
|
212
|
+
}
|
|
213
|
+
if (lowerName === 'rag') {
|
|
214
|
+
return 'retrieval index data';
|
|
215
|
+
}
|
|
216
|
+
if (lowerName === 'summaries') {
|
|
217
|
+
return 'summary files';
|
|
218
|
+
}
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
function orderEntriesForTree(entries) {
|
|
222
|
+
return [...entries].sort((left, right) => {
|
|
223
|
+
if (left.type === 'directory' && right.type !== 'directory') {
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
if (left.type !== 'directory' && right.type === 'directory') {
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
return basename(left.path).localeCompare(basename(right.path));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function formatToolCallTitle(call) {
|
|
233
|
+
const path = typeof call.input['path'] === 'string' ? call.input['path'] : undefined;
|
|
234
|
+
const pattern = typeof call.input['pattern'] === 'string' ? call.input['pattern'] : undefined;
|
|
235
|
+
if (call.toolName === 'glob' && pattern) {
|
|
236
|
+
return `Glob ${path ?? '.'} ${pattern}`;
|
|
237
|
+
}
|
|
238
|
+
const target = path ?? pattern ?? JSON.stringify(call.input);
|
|
239
|
+
const label = call.toolName === 'list_files' ? 'List' : call.toolName === 'read_file' ? 'Read' : call.toolName;
|
|
240
|
+
return `${label} ${target}`;
|
|
241
|
+
}
|
|
242
|
+
function compactToolOutput(output) {
|
|
243
|
+
const parsed = parseToolOutput(output);
|
|
244
|
+
if (!parsed) {
|
|
245
|
+
return truncate(output, 8_000);
|
|
246
|
+
}
|
|
247
|
+
if (Array.isArray(parsed['entries'])) {
|
|
248
|
+
const entries = parsed['entries'].filter(isDirectoryEntry);
|
|
249
|
+
const path = typeof parsed['path'] === 'string' ? parsed['path'] : '';
|
|
250
|
+
const files = entries.filter(entry => entry.type === 'file');
|
|
251
|
+
const directories = entries.filter(entry => entry.type === 'directory');
|
|
252
|
+
return [
|
|
253
|
+
`${entries.length} entries${path ? ` in ${path}` : ''}.`,
|
|
254
|
+
directories.length > 0 ? `Directories: ${directories.map(entry => entry.path).join(', ')}` : '',
|
|
255
|
+
files.length > 0 ? `Files: ${files.map(entry => entry.path).join(', ')}` : '',
|
|
256
|
+
]
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.join('\n');
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(parsed['matches'])) {
|
|
261
|
+
const matches = parsed['matches'].filter((item) => typeof item === 'string');
|
|
262
|
+
return `${matches.length} matched paths:\n${matches.slice(0, 120).join('\n')}${matches.length > 120 ? '\n[truncated]' : ''}`;
|
|
263
|
+
}
|
|
264
|
+
if (typeof parsed['content'] === 'string') {
|
|
265
|
+
const path = typeof parsed['path'] === 'string' ? parsed['path'] : '';
|
|
266
|
+
return `${path ? `File: ${path}\n` : ''}${truncate(parsed['content'], 12_000)}`;
|
|
267
|
+
}
|
|
268
|
+
return truncate(JSON.stringify(parsed), 8_000);
|
|
269
|
+
}
|
|
270
|
+
function parseEntries(result) {
|
|
271
|
+
if (!result.ok) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
const parsed = parseToolOutput(result.output);
|
|
275
|
+
return Array.isArray(parsed?.['entries']) ? parsed['entries'].filter(isDirectoryEntry) : [];
|
|
276
|
+
}
|
|
277
|
+
function parseToolOutput(output) {
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(output);
|
|
280
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function selectGuideFile(entries) {
|
|
287
|
+
const priority = ['AGENTS.md', 'AGENT.md', 'CLAUDE.md', 'REMI.md', 'README.md', 'PLAN.md'];
|
|
288
|
+
for (const name of priority) {
|
|
289
|
+
const found = entries.find(entry => entry.type === 'file' && basename(entry.path) === name);
|
|
290
|
+
if (found) {
|
|
291
|
+
return found.path;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
function selectImportantDirectories(entries) {
|
|
297
|
+
const importantNames = ['.claude', 'summaries', 'rag', 'old', 'archive', 'docs'];
|
|
298
|
+
return entries
|
|
299
|
+
.filter(entry => entry.type === 'directory' && importantNames.includes(basename(entry.path).toLowerCase()))
|
|
300
|
+
.map(entry => entry.path)
|
|
301
|
+
.slice(0, 4);
|
|
302
|
+
}
|
|
303
|
+
function selectContentDirectory(entries) {
|
|
304
|
+
const candidates = ['chapters', 'src', 'apps', 'packages', 'docs'];
|
|
305
|
+
for (const name of candidates) {
|
|
306
|
+
const found = entries.find(entry => entry.type === 'directory' && basename(entry.path).toLowerCase() === name);
|
|
307
|
+
if (found) {
|
|
308
|
+
return found.path;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
function selectSummaryFile(entries) {
|
|
314
|
+
const summariesDir = entries.find(entry => entry.type === 'directory' && basename(entry.path).toLowerCase() === 'summaries');
|
|
315
|
+
return summariesDir ? `${summariesDir.path}/chapter_summaries.md` : undefined;
|
|
316
|
+
}
|
|
317
|
+
function isDirectoryEntry(value) {
|
|
318
|
+
if (!value || typeof value !== 'object') {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const entry = value;
|
|
322
|
+
return typeof entry.path === 'string' && typeof entry.type === 'string';
|
|
323
|
+
}
|
|
324
|
+
function basename(path) {
|
|
325
|
+
return path.split('/').filter(Boolean).at(-1) ?? path;
|
|
326
|
+
}
|
|
327
|
+
function dirname(path) {
|
|
328
|
+
const normalized = path.replace(/\/+$/, '');
|
|
329
|
+
const slashIndex = normalized.lastIndexOf('/');
|
|
330
|
+
if (slashIndex < 0) {
|
|
331
|
+
return '.';
|
|
332
|
+
}
|
|
333
|
+
if (slashIndex === 0) {
|
|
334
|
+
return '/';
|
|
335
|
+
}
|
|
336
|
+
return normalized.slice(0, slashIndex);
|
|
337
|
+
}
|
|
338
|
+
function formatBytes(bytes) {
|
|
339
|
+
if (bytes >= 1024 * 1024) {
|
|
340
|
+
return `${formatByteNumber(bytes / (1024 * 1024))}MB`;
|
|
341
|
+
}
|
|
342
|
+
if (bytes >= 1024) {
|
|
343
|
+
return `${formatByteNumber(bytes / 1024)}KB`;
|
|
344
|
+
}
|
|
345
|
+
return `${bytes}B`;
|
|
346
|
+
}
|
|
347
|
+
function formatByteNumber(value) {
|
|
348
|
+
return value >= 10 ? String(Math.round(value)) : value.toFixed(1);
|
|
349
|
+
}
|
|
350
|
+
function truncate(text, maxChars) {
|
|
351
|
+
return text.length > maxChars ? `${text.slice(0, maxChars - 14)}\n[truncated]` : text;
|
|
352
|
+
}
|
|
353
|
+
function allowedReadRoots(cwd, env) {
|
|
354
|
+
const roots = [cwd];
|
|
355
|
+
if (env.HOME && env.HOME !== cwd) {
|
|
356
|
+
roots.push(env.HOME);
|
|
357
|
+
}
|
|
358
|
+
return roots;
|
|
359
|
+
}
|