@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67
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/CHANGELOG.md +60 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +54 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +22 -24
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +63 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +604 -578
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +73 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +257 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +239 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +6 -2
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +108 -47
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +42 -0
- package/src/modes/interactive/components/tool-execution.ts +46 -8
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -345
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff generation and replace-mode utilities for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* Provides diff string generation and the replace-mode edit logic
|
|
5
|
+
* used when not in patch mode.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Diff from "diff";
|
|
9
|
+
import { resolveToCwd } from "../path-utils";
|
|
10
|
+
import { previewPatch } from "./applicator";
|
|
11
|
+
import { DEFAULT_FUZZY_THRESHOLD, findMatch } from "./fuzzy";
|
|
12
|
+
import { adjustIndentation, normalizeToLF, stripBom } from "./normalize";
|
|
13
|
+
import type { DiffError, DiffResult, PatchInput } from "./types";
|
|
14
|
+
import { EditMatchError } from "./types";
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// Diff String Generation
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a unified diff string with line numbers and context.
|
|
22
|
+
* Returns both the diff string and the first changed line number (in the new file).
|
|
23
|
+
*/
|
|
24
|
+
export function generateDiffString(oldContent: string, newContent: string, contextLines = 4): DiffResult {
|
|
25
|
+
const parts = Diff.diffLines(oldContent, newContent);
|
|
26
|
+
const output: string[] = [];
|
|
27
|
+
|
|
28
|
+
const countLines = (content: string): number => {
|
|
29
|
+
const lines = content.split("\n");
|
|
30
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") {
|
|
31
|
+
lines.pop();
|
|
32
|
+
}
|
|
33
|
+
return Math.max(1, lines.length);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const maxLineNum = Math.max(countLines(oldContent), countLines(newContent));
|
|
37
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
38
|
+
|
|
39
|
+
let oldLineNum = 1;
|
|
40
|
+
let newLineNum = 1;
|
|
41
|
+
let lastWasChange = false;
|
|
42
|
+
let firstChangedLine: number | undefined;
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < parts.length; i++) {
|
|
45
|
+
const part = parts[i];
|
|
46
|
+
const raw = part.value.split("\n");
|
|
47
|
+
if (raw[raw.length - 1] === "") {
|
|
48
|
+
raw.pop();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (part.added || part.removed) {
|
|
52
|
+
// Capture the first changed line (in the new file)
|
|
53
|
+
if (firstChangedLine === undefined) {
|
|
54
|
+
firstChangedLine = newLineNum;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Show the change
|
|
58
|
+
for (const line of raw) {
|
|
59
|
+
if (part.added) {
|
|
60
|
+
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
61
|
+
output.push(`+${lineNum} ${line}`);
|
|
62
|
+
newLineNum++;
|
|
63
|
+
} else {
|
|
64
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
65
|
+
output.push(`-${lineNum} ${line}`);
|
|
66
|
+
oldLineNum++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lastWasChange = true;
|
|
70
|
+
} else {
|
|
71
|
+
// Context lines - only show a few before/after changes
|
|
72
|
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
73
|
+
|
|
74
|
+
if (lastWasChange || nextPartIsChange) {
|
|
75
|
+
let linesToShow = raw;
|
|
76
|
+
let skipStart = 0;
|
|
77
|
+
let skipEnd = 0;
|
|
78
|
+
|
|
79
|
+
if (!lastWasChange) {
|
|
80
|
+
// Show only last N lines as leading context
|
|
81
|
+
skipStart = Math.max(0, raw.length - contextLines);
|
|
82
|
+
linesToShow = raw.slice(skipStart);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
86
|
+
// Show only first N lines as trailing context
|
|
87
|
+
skipEnd = linesToShow.length - contextLines;
|
|
88
|
+
linesToShow = linesToShow.slice(0, contextLines);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add ellipsis if we skipped lines at start
|
|
92
|
+
if (skipStart > 0) {
|
|
93
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
94
|
+
oldLineNum += skipStart;
|
|
95
|
+
newLineNum += skipStart;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const line of linesToShow) {
|
|
99
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
100
|
+
output.push(` ${lineNum} ${line}`);
|
|
101
|
+
oldLineNum++;
|
|
102
|
+
newLineNum++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add ellipsis if we skipped lines at end
|
|
106
|
+
if (skipEnd > 0) {
|
|
107
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
108
|
+
oldLineNum += skipEnd;
|
|
109
|
+
newLineNum += skipEnd;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Skip these context lines entirely
|
|
113
|
+
oldLineNum += raw.length;
|
|
114
|
+
newLineNum += raw.length;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
lastWasChange = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { diff: output.join("\n"), firstChangedLine };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
125
|
+
// Replace Mode Logic
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
127
|
+
|
|
128
|
+
export interface ReplaceOptions {
|
|
129
|
+
/** Allow fuzzy matching */
|
|
130
|
+
fuzzy: boolean;
|
|
131
|
+
/** Replace all occurrences */
|
|
132
|
+
all: boolean;
|
|
133
|
+
/** Similarity threshold for fuzzy matching */
|
|
134
|
+
threshold?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ReplaceResult {
|
|
138
|
+
/** The new content after replacements */
|
|
139
|
+
content: string;
|
|
140
|
+
/** Number of replacements made */
|
|
141
|
+
count: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate a unified diff string without file headers.
|
|
146
|
+
* Returns both the diff string and the first changed line number (in the new file).
|
|
147
|
+
*/
|
|
148
|
+
export function generateUnifiedDiffString(oldContent: string, newContent: string, contextLines = 3): DiffResult {
|
|
149
|
+
const patch = Diff.structuredPatch("", "", oldContent, newContent, "", "", { context: contextLines });
|
|
150
|
+
const output: string[] = [];
|
|
151
|
+
let firstChangedLine: number | undefined;
|
|
152
|
+
|
|
153
|
+
for (const hunk of patch.hunks) {
|
|
154
|
+
output.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
155
|
+
let newLine = hunk.newStart;
|
|
156
|
+
for (const line of hunk.lines) {
|
|
157
|
+
output.push(line);
|
|
158
|
+
if (firstChangedLine === undefined && (line.startsWith("+") || line.startsWith("-"))) {
|
|
159
|
+
firstChangedLine = newLine;
|
|
160
|
+
}
|
|
161
|
+
if (line.startsWith("+") || line.startsWith(" ")) {
|
|
162
|
+
newLine++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { diff: output.join("\n"), firstChangedLine };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find and replace text in content using fuzzy matching.
|
|
172
|
+
*/
|
|
173
|
+
export function replaceText(content: string, oldText: string, newText: string, options: ReplaceOptions): ReplaceResult {
|
|
174
|
+
if (oldText.length === 0) {
|
|
175
|
+
throw new Error("oldText must not be empty.");
|
|
176
|
+
}
|
|
177
|
+
const threshold = options.threshold ?? DEFAULT_FUZZY_THRESHOLD;
|
|
178
|
+
let normalizedContent = normalizeToLF(content);
|
|
179
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
180
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
181
|
+
let count = 0;
|
|
182
|
+
|
|
183
|
+
if (options.all) {
|
|
184
|
+
// Check for exact matches first
|
|
185
|
+
const exactCount = normalizedContent.split(normalizedOldText).length - 1;
|
|
186
|
+
if (exactCount > 0) {
|
|
187
|
+
return {
|
|
188
|
+
content: normalizedContent.split(normalizedOldText).join(normalizedNewText),
|
|
189
|
+
count: exactCount,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// No exact matches - try fuzzy matching iteratively
|
|
194
|
+
while (true) {
|
|
195
|
+
const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
|
|
196
|
+
allowFuzzy: options.fuzzy,
|
|
197
|
+
threshold,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const shouldUseClosest =
|
|
201
|
+
options.fuzzy &&
|
|
202
|
+
matchOutcome.closest &&
|
|
203
|
+
matchOutcome.closest.confidence >= threshold &&
|
|
204
|
+
(matchOutcome.fuzzyMatches === undefined || matchOutcome.fuzzyMatches <= 1);
|
|
205
|
+
const match = matchOutcome.match || (shouldUseClosest ? matchOutcome.closest : undefined);
|
|
206
|
+
if (!match) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const adjustedNewText = adjustIndentation(normalizedOldText, match.actualText, normalizedNewText);
|
|
211
|
+
if (adjustedNewText === match.actualText) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
normalizedContent =
|
|
215
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
216
|
+
adjustedNewText +
|
|
217
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
218
|
+
count++;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { content: normalizedContent, count };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Single replacement mode
|
|
225
|
+
const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
|
|
226
|
+
allowFuzzy: options.fuzzy,
|
|
227
|
+
threshold,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Found ${matchOutcome.occurrences} occurrences of the text. ` +
|
|
233
|
+
`The text must be unique. Please provide more context to make it unique, or use all: true to replace all.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!matchOutcome.match) {
|
|
238
|
+
return { content: normalizedContent, count: 0 };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const match = matchOutcome.match;
|
|
242
|
+
const adjustedNewText = adjustIndentation(normalizedOldText, match.actualText, normalizedNewText);
|
|
243
|
+
normalizedContent =
|
|
244
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
245
|
+
adjustedNewText +
|
|
246
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
247
|
+
|
|
248
|
+
return { content: normalizedContent, count: 1 };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
252
|
+
// Preview/Diff Computation
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compute the diff for an edit operation without applying it.
|
|
257
|
+
* Used for preview rendering in the TUI before the tool executes.
|
|
258
|
+
*/
|
|
259
|
+
export async function computeEditDiff(
|
|
260
|
+
path: string,
|
|
261
|
+
oldText: string,
|
|
262
|
+
newText: string,
|
|
263
|
+
cwd: string,
|
|
264
|
+
fuzzy = true,
|
|
265
|
+
all = false,
|
|
266
|
+
threshold?: number,
|
|
267
|
+
): Promise<DiffResult | DiffError> {
|
|
268
|
+
if (oldText.length === 0) {
|
|
269
|
+
return { error: "oldText must not be empty." };
|
|
270
|
+
}
|
|
271
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const file = Bun.file(absolutePath);
|
|
275
|
+
try {
|
|
276
|
+
if (!(await file.exists())) {
|
|
277
|
+
return { error: `File not found: ${path}` };
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
return { error: `File not found: ${path}` };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let rawContent: string;
|
|
284
|
+
try {
|
|
285
|
+
rawContent = await file.text();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
+
return { error: message || `Unable to read ${path}` };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const { text: content } = stripBom(rawContent);
|
|
292
|
+
const normalizedContent = normalizeToLF(content);
|
|
293
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
294
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
295
|
+
|
|
296
|
+
const result = replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
|
|
297
|
+
fuzzy,
|
|
298
|
+
all,
|
|
299
|
+
threshold,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (result.count === 0) {
|
|
303
|
+
// Get closest match for error message
|
|
304
|
+
const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
|
|
305
|
+
allowFuzzy: fuzzy,
|
|
306
|
+
threshold: threshold ?? DEFAULT_FUZZY_THRESHOLD,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
310
|
+
return {
|
|
311
|
+
error: `Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique, or use all: true to replace all.`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
|
|
317
|
+
allowFuzzy: fuzzy,
|
|
318
|
+
threshold: threshold ?? DEFAULT_FUZZY_THRESHOLD,
|
|
319
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (normalizedContent === result.content) {
|
|
325
|
+
return {
|
|
326
|
+
error: `No changes would be made to ${path}. The replacement produces identical content.`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return generateDiffString(normalizedContent, result.content);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Compute the diff for a patch operation without applying it.
|
|
338
|
+
* Used for preview rendering in the TUI before patch-mode edits execute.
|
|
339
|
+
*/
|
|
340
|
+
export async function computePatchDiff(
|
|
341
|
+
input: PatchInput,
|
|
342
|
+
cwd: string,
|
|
343
|
+
options?: { fuzzyThreshold?: number; allowFuzzy?: boolean },
|
|
344
|
+
): Promise<DiffResult | DiffError> {
|
|
345
|
+
try {
|
|
346
|
+
const result = await previewPatch(input, {
|
|
347
|
+
cwd,
|
|
348
|
+
fuzzyThreshold: options?.fuzzyThreshold,
|
|
349
|
+
allowFuzzy: options?.allowFuzzy,
|
|
350
|
+
});
|
|
351
|
+
const oldContent = result.change.oldContent ?? "";
|
|
352
|
+
const newContent = result.change.newContent ?? "";
|
|
353
|
+
const normalizedOld = normalizeToLF(stripBom(oldContent).text);
|
|
354
|
+
const normalizedNew = normalizeToLF(stripBom(newContent).text);
|
|
355
|
+
if (!normalizedOld && !normalizedNew) {
|
|
356
|
+
return { diff: "", firstChangedLine: undefined };
|
|
357
|
+
}
|
|
358
|
+
return generateUnifiedDiffString(normalizedOld, normalizedNew);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
361
|
+
}
|
|
362
|
+
}
|