pi-repoprompt-cli 0.2.0 → 0.2.6
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 +44 -2
- package/extensions/repoprompt-cli/README.md +94 -0
- package/extensions/repoprompt-cli/config.json.example +3 -0
- package/extensions/repoprompt-cli/config.ts +47 -0
- package/extensions/repoprompt-cli/index.ts +1674 -0
- package/extensions/repoprompt-cli/readcache/LICENSE +23 -0
- package/extensions/repoprompt-cli/readcache/constants.ts +38 -0
- package/extensions/repoprompt-cli/readcache/diff.ts +129 -0
- package/extensions/repoprompt-cli/readcache/meta.ts +241 -0
- package/extensions/repoprompt-cli/readcache/object-store.ts +184 -0
- package/extensions/repoprompt-cli/readcache/read-file.ts +438 -0
- package/extensions/repoprompt-cli/readcache/replay.ts +366 -0
- package/extensions/repoprompt-cli/readcache/resolve.ts +235 -0
- package/extensions/repoprompt-cli/readcache/text.ts +43 -0
- package/extensions/repoprompt-cli/readcache/types.ts +73 -0
- package/extensions/repoprompt-cli/types.ts +6 -0
- package/package.json +26 -3
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// readcache/read-file.ts - read_file wrapper with pi-readcache-like behavior
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_EXCLUDED_PATH_PATTERNS,
|
|
10
|
+
MAX_DIFF_FILE_BYTES,
|
|
11
|
+
MAX_DIFF_FILE_LINES,
|
|
12
|
+
SCOPE_FULL,
|
|
13
|
+
scopeRange,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import { computeUnifiedDiff, isDiffUseful } from "./diff.js";
|
|
16
|
+
import { buildRpReadcacheMetaV1 } from "./meta.js";
|
|
17
|
+
import { hashBytes, loadObject, persistObjectIfAbsent } from "./object-store.js";
|
|
18
|
+
import {
|
|
19
|
+
buildKnowledgeForLeaf,
|
|
20
|
+
isRangeScopeBlockedByInvalidation,
|
|
21
|
+
overlaySet,
|
|
22
|
+
type ReplayRuntimeState,
|
|
23
|
+
} from "./replay.js";
|
|
24
|
+
import { compareSlices, splitLines, truncateForReadcache } from "./text.js";
|
|
25
|
+
import type {
|
|
26
|
+
ReadCacheDebugReason,
|
|
27
|
+
ReadCacheDebugV1,
|
|
28
|
+
RpReadcacheMetaV1,
|
|
29
|
+
ScopeKey,
|
|
30
|
+
ScopeTrust,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
import { resolveReadFilePath } from "./resolve.js";
|
|
33
|
+
|
|
34
|
+
const UTF8_STRICT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
35
|
+
|
|
36
|
+
export interface ReadFileArgs {
|
|
37
|
+
path: string;
|
|
38
|
+
start_line?: number;
|
|
39
|
+
limit?: number;
|
|
40
|
+
bypass_cache?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CurrentTextState {
|
|
44
|
+
bytes: Buffer;
|
|
45
|
+
text: string;
|
|
46
|
+
totalLines: number;
|
|
47
|
+
currentHash: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
throw new Error("Operation aborted");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isExcludedPath(pathKey: string): boolean {
|
|
57
|
+
const baseName = basename(pathKey).toLowerCase();
|
|
58
|
+
return DEFAULT_EXCLUDED_PATH_PATTERNS.some((pattern) => {
|
|
59
|
+
if (pattern === ".env*") {
|
|
60
|
+
return baseName.startsWith(".env");
|
|
61
|
+
}
|
|
62
|
+
if (pattern.startsWith("*")) {
|
|
63
|
+
return baseName.endsWith(pattern.slice(1));
|
|
64
|
+
}
|
|
65
|
+
return baseName === pattern;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeStartEnd(
|
|
70
|
+
startLine: number | undefined,
|
|
71
|
+
limit: number | undefined,
|
|
72
|
+
totalLines: number,
|
|
73
|
+
): { start: number; end: number; totalLines: number } {
|
|
74
|
+
if (!Number.isInteger(totalLines) || totalLines <= 0) {
|
|
75
|
+
throw new Error(`Invalid totalLines: ${String(totalLines)}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (startLine === undefined) {
|
|
79
|
+
const end = limit !== undefined ? Math.min(totalLines, Math.max(1, limit)) : totalLines;
|
|
80
|
+
return { start: 1, end, totalLines };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Number.isInteger(startLine) || startLine === 0) {
|
|
84
|
+
throw new Error(`Invalid start_line: ${String(startLine)}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (startLine < 0) {
|
|
88
|
+
const n = Math.abs(startLine);
|
|
89
|
+
const start = Math.max(1, totalLines - n + 1);
|
|
90
|
+
return { start, end: totalLines, totalLines };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const start = Math.min(startLine, totalLines);
|
|
94
|
+
|
|
95
|
+
if (limit === undefined) {
|
|
96
|
+
return { start, end: totalLines, totalLines };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
100
|
+
throw new Error(`Invalid limit: ${String(limit)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const end = Math.min(totalLines, start + limit - 1);
|
|
104
|
+
return { start, end, totalLines };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scopeKeyForRange(start: number, end: number, totalLines: number): ScopeKey {
|
|
108
|
+
if (start <= 1 && end >= totalLines) {
|
|
109
|
+
return SCOPE_FULL;
|
|
110
|
+
}
|
|
111
|
+
return scopeRange(start, end);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildUnchangedMarker(
|
|
115
|
+
scopeKey: ScopeKey,
|
|
116
|
+
start: number,
|
|
117
|
+
end: number,
|
|
118
|
+
totalLines: number,
|
|
119
|
+
outsideRangeChanged: boolean,
|
|
120
|
+
): string {
|
|
121
|
+
if (scopeKey === SCOPE_FULL) {
|
|
122
|
+
return `[readcache: unchanged, ${totalLines} lines]`;
|
|
123
|
+
}
|
|
124
|
+
if (outsideRangeChanged) {
|
|
125
|
+
return `[readcache: unchanged in lines ${start}-${end}; changes exist outside this range]`;
|
|
126
|
+
}
|
|
127
|
+
return `[readcache: unchanged in lines ${start}-${end} of ${totalLines}]`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildDiffPayload(changedLines: number, totalLines: number, diffText: string): string {
|
|
131
|
+
return `[readcache: ${changedLines} lines changed of ${totalLines}]\n${diffText}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildDebugInfo(
|
|
135
|
+
scopeKey: ScopeKey,
|
|
136
|
+
baseHash: string | undefined,
|
|
137
|
+
reason: ReadCacheDebugReason,
|
|
138
|
+
overrides: Partial<Omit<ReadCacheDebugV1, "reason" | "scope" | "baseHashFound" | "diffAttempted">> & {
|
|
139
|
+
diffAttempted?: boolean;
|
|
140
|
+
} = {},
|
|
141
|
+
): ReadCacheDebugV1 {
|
|
142
|
+
return {
|
|
143
|
+
reason,
|
|
144
|
+
scope: scopeKey === SCOPE_FULL ? "full" : "range",
|
|
145
|
+
baseHashFound: baseHash !== undefined,
|
|
146
|
+
diffAttempted: overrides.diffAttempted ?? false,
|
|
147
|
+
...overrides,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function selectBaseTrust(
|
|
152
|
+
pathKnowledge: Map<ScopeKey, ScopeTrust> | undefined,
|
|
153
|
+
scopeKey: ScopeKey,
|
|
154
|
+
rangeScopeBlocked: boolean,
|
|
155
|
+
): ScopeTrust | undefined {
|
|
156
|
+
if (!pathKnowledge) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (scopeKey === SCOPE_FULL) {
|
|
161
|
+
return pathKnowledge.get(SCOPE_FULL);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (rangeScopeBlocked) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const exactTrust = pathKnowledge.get(scopeKey);
|
|
169
|
+
const fullTrust = pathKnowledge.get(SCOPE_FULL);
|
|
170
|
+
|
|
171
|
+
if (!exactTrust) {
|
|
172
|
+
return fullTrust;
|
|
173
|
+
}
|
|
174
|
+
if (!fullTrust) {
|
|
175
|
+
return exactTrust;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return exactTrust.seq >= fullTrust.seq ? exactTrust : fullTrust;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function readCurrentTextStrict(absolutePath: string): Promise<CurrentTextState | undefined> {
|
|
182
|
+
let fileBytes: Buffer;
|
|
183
|
+
try {
|
|
184
|
+
fileBytes = await readFile(absolutePath);
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let text: string;
|
|
190
|
+
try {
|
|
191
|
+
text = UTF8_STRICT_DECODER.decode(fileBytes);
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const totalLines = splitLines(text).length;
|
|
197
|
+
return {
|
|
198
|
+
bytes: fileBytes,
|
|
199
|
+
text,
|
|
200
|
+
totalLines,
|
|
201
|
+
currentHash: hashBytes(fileBytes),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function persistAndOverlay(
|
|
206
|
+
runtimeState: ReplayRuntimeState,
|
|
207
|
+
ctx: ExtensionContext,
|
|
208
|
+
repoRoot: string,
|
|
209
|
+
pathKey: string,
|
|
210
|
+
scopeKey: ScopeKey,
|
|
211
|
+
servedHash: string,
|
|
212
|
+
text: string,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
try {
|
|
215
|
+
await persistObjectIfAbsent(repoRoot, servedHash, text);
|
|
216
|
+
} catch {
|
|
217
|
+
// Fail-open
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
overlaySet(runtimeState, ctx.sessionManager, pathKey, scopeKey, servedHash);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface ReadFileWithCacheResult {
|
|
224
|
+
outputText: string | null;
|
|
225
|
+
meta: RpReadcacheMetaV1 | null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function readFileWithCache(
|
|
229
|
+
pi: ExtensionAPI,
|
|
230
|
+
args: ReadFileArgs,
|
|
231
|
+
ctx: ExtensionContext,
|
|
232
|
+
runtimeState: ReplayRuntimeState,
|
|
233
|
+
windowId: number,
|
|
234
|
+
tab?: string,
|
|
235
|
+
signal?: AbortSignal,
|
|
236
|
+
): Promise<ReadFileWithCacheResult> {
|
|
237
|
+
throwIfAborted(signal);
|
|
238
|
+
|
|
239
|
+
const resolved = await resolveReadFilePath(pi, args.path, ctx.cwd, windowId, tab);
|
|
240
|
+
if (!resolved.absolutePath) {
|
|
241
|
+
return { outputText: null, meta: null };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (isExcludedPath(resolved.absolutePath)) {
|
|
245
|
+
return { outputText: null, meta: null };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const current = await readCurrentTextStrict(resolved.absolutePath);
|
|
249
|
+
if (!current) {
|
|
250
|
+
return { outputText: null, meta: null };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const repoRoot = resolved.repoRoot ?? ctx.cwd;
|
|
254
|
+
|
|
255
|
+
let start: number;
|
|
256
|
+
let end: number;
|
|
257
|
+
let totalLines: number;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const normalized = normalizeStartEnd(args.start_line, args.limit, current.totalLines);
|
|
261
|
+
start = normalized.start;
|
|
262
|
+
end = normalized.end;
|
|
263
|
+
totalLines = normalized.totalLines;
|
|
264
|
+
} catch {
|
|
265
|
+
return { outputText: null, meta: null };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const pathKey = resolved.absolutePath;
|
|
269
|
+
const scopeKey = scopeKeyForRange(start, end, totalLines);
|
|
270
|
+
|
|
271
|
+
if (args.bypass_cache === true) {
|
|
272
|
+
const meta = buildRpReadcacheMetaV1({
|
|
273
|
+
pathKey,
|
|
274
|
+
scopeKey,
|
|
275
|
+
servedHash: current.currentHash,
|
|
276
|
+
mode: "full",
|
|
277
|
+
totalLines,
|
|
278
|
+
rangeStart: start,
|
|
279
|
+
rangeEnd: end,
|
|
280
|
+
bytes: current.bytes.byteLength,
|
|
281
|
+
debug: buildDebugInfo(scopeKey, undefined, "bypass_cache"),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await persistAndOverlay(runtimeState, ctx, repoRoot, pathKey, scopeKey, current.currentHash, current.text);
|
|
285
|
+
return { outputText: null, meta };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const knowledge = buildKnowledgeForLeaf(ctx.sessionManager, runtimeState);
|
|
289
|
+
const pathKnowledge = knowledge.get(pathKey);
|
|
290
|
+
const rangeScopeBlocked = isRangeScopeBlockedByInvalidation(ctx.sessionManager, runtimeState, pathKey, scopeKey);
|
|
291
|
+
const baseHash = selectBaseTrust(pathKnowledge, scopeKey, rangeScopeBlocked)?.hash;
|
|
292
|
+
|
|
293
|
+
if (!baseHash) {
|
|
294
|
+
const meta = buildRpReadcacheMetaV1({
|
|
295
|
+
pathKey,
|
|
296
|
+
scopeKey,
|
|
297
|
+
servedHash: current.currentHash,
|
|
298
|
+
mode: "full",
|
|
299
|
+
totalLines,
|
|
300
|
+
rangeStart: start,
|
|
301
|
+
rangeEnd: end,
|
|
302
|
+
bytes: current.bytes.byteLength,
|
|
303
|
+
debug: buildDebugInfo(scopeKey, baseHash, "no_base_hash"),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await persistAndOverlay(runtimeState, ctx, repoRoot, pathKey, scopeKey, current.currentHash, current.text);
|
|
307
|
+
return { outputText: null, meta };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (baseHash === current.currentHash) {
|
|
311
|
+
const mode = scopeKey === SCOPE_FULL ? "unchanged" : "unchanged_range";
|
|
312
|
+
const meta = buildRpReadcacheMetaV1({
|
|
313
|
+
pathKey,
|
|
314
|
+
scopeKey,
|
|
315
|
+
servedHash: current.currentHash,
|
|
316
|
+
baseHash,
|
|
317
|
+
mode,
|
|
318
|
+
totalLines,
|
|
319
|
+
rangeStart: start,
|
|
320
|
+
rangeEnd: end,
|
|
321
|
+
bytes: current.bytes.byteLength,
|
|
322
|
+
debug: buildDebugInfo(scopeKey, baseHash, "hash_match"),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const marker = buildUnchangedMarker(scopeKey, start, end, totalLines, false);
|
|
326
|
+
await persistAndOverlay(runtimeState, ctx, repoRoot, pathKey, scopeKey, current.currentHash, current.text);
|
|
327
|
+
|
|
328
|
+
return { outputText: marker, meta };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const baseText = await loadObject(repoRoot, baseHash);
|
|
332
|
+
|
|
333
|
+
const fallback = async (
|
|
334
|
+
reason: ReadCacheDebugReason,
|
|
335
|
+
overrides: Partial<Omit<ReadCacheDebugV1, "reason" | "scope" | "baseHashFound" | "diffAttempted">> & {
|
|
336
|
+
diffAttempted?: boolean;
|
|
337
|
+
} = {},
|
|
338
|
+
): Promise<ReadFileWithCacheResult> => {
|
|
339
|
+
const meta = buildRpReadcacheMetaV1({
|
|
340
|
+
pathKey,
|
|
341
|
+
scopeKey,
|
|
342
|
+
servedHash: current.currentHash,
|
|
343
|
+
baseHash,
|
|
344
|
+
mode: "baseline_fallback",
|
|
345
|
+
totalLines,
|
|
346
|
+
rangeStart: start,
|
|
347
|
+
rangeEnd: end,
|
|
348
|
+
bytes: current.bytes.byteLength,
|
|
349
|
+
debug: buildDebugInfo(scopeKey, baseHash, reason, overrides),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await persistAndOverlay(runtimeState, ctx, repoRoot, pathKey, scopeKey, current.currentHash, current.text);
|
|
353
|
+
return { outputText: null, meta };
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
if (!baseText) {
|
|
357
|
+
return fallback("base_object_missing", { baseObjectFound: false });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (scopeKey !== SCOPE_FULL) {
|
|
361
|
+
if (compareSlices(baseText, current.text, start, end)) {
|
|
362
|
+
const meta = buildRpReadcacheMetaV1({
|
|
363
|
+
pathKey,
|
|
364
|
+
scopeKey,
|
|
365
|
+
servedHash: current.currentHash,
|
|
366
|
+
baseHash,
|
|
367
|
+
mode: "unchanged_range",
|
|
368
|
+
totalLines,
|
|
369
|
+
rangeStart: start,
|
|
370
|
+
rangeEnd: end,
|
|
371
|
+
bytes: current.bytes.byteLength,
|
|
372
|
+
debug: buildDebugInfo(scopeKey, baseHash, "range_slice_unchanged", { outsideRangeChanged: true }),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const marker = buildUnchangedMarker(scopeKey, start, end, totalLines, true);
|
|
376
|
+
await persistAndOverlay(runtimeState, ctx, repoRoot, pathKey, scopeKey, current.currentHash, current.text);
|
|
377
|
+
|
|
378
|
+
return { outputText: marker, meta };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return fallback("range_slice_changed", { outsideRangeChanged: true });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const baseBytes = Buffer.byteLength(baseText, "utf-8");
|
|
385
|
+
const largestBytes = Math.max(baseBytes, current.bytes.byteLength);
|
|
386
|
+
if (largestBytes > MAX_DIFF_FILE_BYTES) {
|
|
387
|
+
return fallback("diff_file_too_large_bytes", { diffAttempted: true, largestBytes });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const maxLines = Math.max(splitLines(baseText).length, totalLines);
|
|
391
|
+
if (maxLines > MAX_DIFF_FILE_LINES) {
|
|
392
|
+
return fallback("diff_file_too_large_lines", { diffAttempted: true, maxLines });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const diff = computeUnifiedDiff(baseText, current.text, args.path);
|
|
396
|
+
if (!diff) {
|
|
397
|
+
return fallback("diff_unavailable_or_empty", { diffAttempted: true });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!isDiffUseful(diff.diffText, baseText, current.text)) {
|
|
401
|
+
return fallback("diff_not_useful", { diffAttempted: true, diffBytes: diff.diffBytes });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const diffPayload = buildDiffPayload(diff.changedLines, totalLines, diff.diffText);
|
|
405
|
+
const truncation = truncateForReadcache(diffPayload);
|
|
406
|
+
|
|
407
|
+
if (truncation.truncated) {
|
|
408
|
+
return fallback("diff_payload_truncated", {
|
|
409
|
+
diffAttempted: true,
|
|
410
|
+
diffBytes: diff.diffBytes,
|
|
411
|
+
diffChangedLines: diff.changedLines,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const meta = buildRpReadcacheMetaV1({
|
|
416
|
+
pathKey,
|
|
417
|
+
scopeKey,
|
|
418
|
+
servedHash: current.currentHash,
|
|
419
|
+
baseHash,
|
|
420
|
+
mode: "diff",
|
|
421
|
+
totalLines,
|
|
422
|
+
rangeStart: start,
|
|
423
|
+
rangeEnd: end,
|
|
424
|
+
bytes: current.bytes.byteLength,
|
|
425
|
+
debug: buildDebugInfo(scopeKey, baseHash, "diff_emitted", {
|
|
426
|
+
diffAttempted: true,
|
|
427
|
+
diffBytes: diff.diffBytes,
|
|
428
|
+
diffChangedLines: diff.changedLines,
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await persistAndOverlay(runtimeState, ctx, repoRoot, pathKey, scopeKey, current.currentHash, current.text);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
outputText: truncation.content,
|
|
436
|
+
meta,
|
|
437
|
+
};
|
|
438
|
+
}
|