tinker-agent 1.0.65

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 (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. package/src/tui/view-file.ts +122 -0
@@ -0,0 +1,142 @@
1
+ import { Box, Text, useInput } from "ink";
2
+ import { useState } from "react";
3
+ import type { ModelProfile } from "../../cli/model-profiles";
4
+
5
+ export type ModelPickerProps = {
6
+ profiles: readonly ModelProfile[];
7
+ currentProfileName?: string;
8
+ isSwitching?: boolean;
9
+ error?: string;
10
+ onCancel: () => void;
11
+ onSelect: (profile: ModelProfile) => void;
12
+ };
13
+
14
+ export function ModelPicker(props: ModelPickerProps) {
15
+ if (props.profiles.length === 0) {
16
+ throw new Error("ModelPicker requires at least one profile.");
17
+ }
18
+
19
+ return <ModelPickerContent {...props} />;
20
+ }
21
+
22
+ function ModelPickerContent(props: ModelPickerProps) {
23
+ const [selectedIndex, setSelectedIndex] = useState(() =>
24
+ initialSelectedIndex(props.profiles, props.currentProfileName),
25
+ );
26
+
27
+ useInput(
28
+ (input, key) => {
29
+ if (props.isSwitching === true) {
30
+ return;
31
+ }
32
+
33
+ if (key.escape) {
34
+ props.onCancel();
35
+ return;
36
+ }
37
+
38
+ if (key.return) {
39
+ const selected = props.profiles[selectedIndex];
40
+ if (selected !== undefined) {
41
+ props.onSelect(selected);
42
+ }
43
+ return;
44
+ }
45
+
46
+ const direction =
47
+ key.upArrow || (input === "k" && !key.ctrl && !key.meta)
48
+ ? -1
49
+ : key.downArrow || (input === "j" && !key.ctrl && !key.meta)
50
+ ? 1
51
+ : 0;
52
+ if (direction === 0) {
53
+ return;
54
+ }
55
+
56
+ setSelectedIndex((current) =>
57
+ clamp(current + direction, 0, props.profiles.length - 1),
58
+ );
59
+ },
60
+ { isActive: props.isSwitching !== true },
61
+ );
62
+
63
+ return (
64
+ <Box flexDirection="column">
65
+ <Text bold>Switch model profile</Text>
66
+ <Text dimColor>
67
+ {props.isSwitching === true
68
+ ? "Switching…"
69
+ : "↑/↓ or j/k to move · Enter to select · Esc to cancel"}
70
+ </Text>
71
+ {props.profiles.map((profile, index) => (
72
+ <ProfileOption
73
+ key={profile.name}
74
+ profile={profile}
75
+ isSelected={index === selectedIndex}
76
+ isCurrent={profile.name === props.currentProfileName}
77
+ />
78
+ ))}
79
+ {props.error === undefined ? null : (
80
+ <Text color="red" wrap="truncate-end">
81
+ Switch failed: {props.error}
82
+ </Text>
83
+ )}
84
+ <Text dimColor>
85
+ Switching creates a new session; the current session is preserved.
86
+ </Text>
87
+ </Box>
88
+ );
89
+ }
90
+
91
+ function ProfileOption(props: {
92
+ profile: ModelProfile;
93
+ isSelected: boolean;
94
+ isCurrent: boolean;
95
+ }) {
96
+ const marker = props.isSelected ? "❯ " : " ";
97
+
98
+ return (
99
+ <Box flexDirection="column">
100
+ <Text
101
+ color={props.isSelected ? "cyan" : undefined}
102
+ bold={props.isSelected}
103
+ wrap="truncate-end"
104
+ >
105
+ {marker}
106
+ {props.profile.name}
107
+ {props.isCurrent ? " (current)" : ""}
108
+ </Text>
109
+ <Box marginLeft={2} overflow="hidden">
110
+ <Text dimColor wrap="truncate-end">
111
+ {props.profile.model} · {formatTokenCount(props.profile.contextWindowTokens)}{" "}
112
+ context · {formatTokenCount(props.profile.maxSupportedOutputTokens)} output
113
+ </Text>
114
+ </Box>
115
+ </Box>
116
+ );
117
+ }
118
+
119
+ function initialSelectedIndex(
120
+ profiles: readonly ModelProfile[],
121
+ currentProfileName?: string,
122
+ ): number {
123
+ if (currentProfileName === undefined) {
124
+ return 0;
125
+ }
126
+ const index = profiles.findIndex((p) => p.name === currentProfileName);
127
+ return index === -1 ? 0 : index;
128
+ }
129
+
130
+ function clamp(value: number, minimum: number, maximum: number): number {
131
+ return Math.min(Math.max(value, minimum), maximum);
132
+ }
133
+
134
+ function formatTokenCount(tokens: number): string {
135
+ if (tokens >= 1_048_576) {
136
+ return `${(tokens / 1_048_576).toFixed(0)}M`;
137
+ }
138
+ if (tokens >= 1_024) {
139
+ return `${(tokens / 1_024).toFixed(0)}K`;
140
+ }
141
+ return String(tokens);
142
+ }
@@ -0,0 +1,432 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { Box, Text, useInput, usePaste } from "ink";
4
+ import { useState } from "react";
5
+ import type { ContextUsageSnapshot } from "../../agent/context-meter";
6
+ import { formatContextUsageLine } from "../context-format";
7
+ import {
8
+ backspace,
9
+ createLineEditorState,
10
+ deleteForward,
11
+ deleteToLineStart,
12
+ insert,
13
+ type LineEditorState,
14
+ moveDown,
15
+ moveLeft,
16
+ moveRight,
17
+ moveToLineEnd,
18
+ moveToLineStart,
19
+ moveUp,
20
+ splitAtCursor,
21
+ } from "../line-editor";
22
+ import { matchSlashCommands, type SlashCommand } from "../slash-commands";
23
+
24
+ export type PromptInputProps = {
25
+ modelName: string;
26
+ workspaceRoot: string;
27
+ gitBranch?: string;
28
+ contextUsage?: ContextUsageSnapshot;
29
+ isDisabled?: boolean;
30
+ placeholder?: string;
31
+ history?: { entries: readonly string[] };
32
+ commands?: readonly SlashCommand[];
33
+ onSubmit: (value: string) => void;
34
+ };
35
+
36
+ type HistoryNavigation = {
37
+ index: number;
38
+ originalDraft: LineEditorState;
39
+ browsing: boolean;
40
+ };
41
+
42
+ type PromptInputState = {
43
+ editor: LineEditorState;
44
+ navigation?: HistoryNavigation;
45
+ suggestionIndex: number;
46
+ suggestionsDismissed: boolean;
47
+ };
48
+
49
+ function createPromptInputState(value = ""): PromptInputState {
50
+ return {
51
+ editor: createLineEditorState(value),
52
+ suggestionIndex: 0,
53
+ suggestionsDismissed: false,
54
+ };
55
+ }
56
+
57
+ export function PromptInput(props: PromptInputProps) {
58
+ const [state, setState] = useState<PromptInputState>(createPromptInputState);
59
+
60
+ const suggestions = state.suggestionsDismissed
61
+ ? []
62
+ : matchSlashCommands(state.editor.value, props.commands);
63
+ const selectedIndex =
64
+ suggestions.length === 0
65
+ ? 0
66
+ : Math.min(state.suggestionIndex, suggestions.length - 1);
67
+
68
+ const submit = (value: string) => {
69
+ props.onSubmit(value);
70
+ setState(createPromptInputState());
71
+ };
72
+
73
+ const updateEditor = (update: (editor: LineEditorState) => LineEditorState) => {
74
+ setState((current) => {
75
+ const editor = update(current.editor);
76
+
77
+ if (!editorChanged(current.editor, editor)) {
78
+ return current;
79
+ }
80
+
81
+ return applyEditorChange(current, editor);
82
+ });
83
+ };
84
+
85
+ const navigateUp = () => {
86
+ setState((current) => {
87
+ const entries = props.history?.entries ?? [];
88
+
89
+ if (current.navigation?.browsing === true) {
90
+ return navigateHistoryUp(current, entries);
91
+ }
92
+
93
+ const editor = moveUp(current.editor);
94
+ if (editorChanged(current.editor, editor)) {
95
+ return applyEditorChange(current, editor);
96
+ }
97
+
98
+ return navigateHistoryUp(current, entries);
99
+ });
100
+ };
101
+
102
+ const navigateDown = () => {
103
+ setState((current) => {
104
+ const entries = props.history?.entries ?? [];
105
+
106
+ if (current.navigation?.browsing === true) {
107
+ return navigateHistoryDown(current, entries);
108
+ }
109
+
110
+ const editor = moveDown(current.editor);
111
+ return editorChanged(current.editor, editor)
112
+ ? applyEditorChange(current, editor)
113
+ : navigateHistoryDown(current, entries);
114
+ });
115
+ };
116
+
117
+ useInput(
118
+ (input, key) => {
119
+ const selected = suggestions[selectedIndex];
120
+
121
+ if (key.return) {
122
+ if (selected !== undefined) {
123
+ submit(`/${selected.name}`);
124
+ return;
125
+ }
126
+
127
+ submit(state.editor.value);
128
+ return;
129
+ }
130
+
131
+ if (key.tab) {
132
+ if (selected !== undefined) {
133
+ setState(createPromptInputState(`/${selected.name} `));
134
+ }
135
+ return;
136
+ }
137
+
138
+ if (key.escape) {
139
+ if (suggestions.length > 0) {
140
+ setState((current) => ({ ...current, suggestionsDismissed: true }));
141
+ }
142
+ return;
143
+ }
144
+
145
+ if (key.upArrow) {
146
+ if (suggestions.length > 0) {
147
+ setState((current) => ({
148
+ ...current,
149
+ suggestionIndex:
150
+ (selectedIndex + suggestions.length - 1) % suggestions.length,
151
+ }));
152
+ return;
153
+ }
154
+
155
+ navigateUp();
156
+ return;
157
+ }
158
+
159
+ if (key.downArrow) {
160
+ if (suggestions.length > 0) {
161
+ setState((current) => ({
162
+ ...current,
163
+ suggestionIndex: (selectedIndex + 1) % suggestions.length,
164
+ }));
165
+ return;
166
+ }
167
+
168
+ navigateDown();
169
+ return;
170
+ }
171
+
172
+ if (key.leftArrow) {
173
+ updateEditor(moveLeft);
174
+ return;
175
+ }
176
+
177
+ if (key.rightArrow) {
178
+ updateEditor(moveRight);
179
+ return;
180
+ }
181
+
182
+ if (key.backspace || key.delete) {
183
+ updateEditor(backspace);
184
+ return;
185
+ }
186
+
187
+ if (key.ctrl) {
188
+ if (input === "a") {
189
+ updateEditor(moveToLineStart);
190
+ } else if (input === "e") {
191
+ updateEditor(moveToLineEnd);
192
+ } else if (input === "d") {
193
+ updateEditor(deleteForward);
194
+ } else if (input === "u") {
195
+ updateEditor(deleteToLineStart);
196
+ }
197
+ return;
198
+ }
199
+
200
+ if (key.meta || key.pageUp || key.pageDown) {
201
+ return;
202
+ }
203
+
204
+ if (input !== "") {
205
+ updateEditor((editor) => insert(editor, normalizeLineBreaks(input)));
206
+ }
207
+ },
208
+ { isActive: props.isDisabled !== true },
209
+ );
210
+
211
+ usePaste(
212
+ (text) => {
213
+ updateEditor((editor) => insert(editor, normalizeLineBreaks(text)));
214
+ },
215
+ { isActive: props.isDisabled !== true },
216
+ );
217
+
218
+ const showSuggestions = suggestions.length > 0 && props.isDisabled !== true;
219
+
220
+ return (
221
+ <Box flexDirection="column">
222
+ <Box width="100%" borderStyle="single" borderLeft={false} borderRight={false}>
223
+ {renderEditor(state.editor, props)}
224
+ </Box>
225
+ {showSuggestions ? null : (
226
+ <Box>
227
+ <Text dimColor>
228
+ {props.modelName} · {formatWorkspacePath(props.workspaceRoot)}
229
+ {props.gitBranch === undefined ? null : ` · ${props.gitBranch}`}
230
+ </Text>
231
+ {props.contextUsage === undefined ? null : (
232
+ <>
233
+ <Text dimColor> · </Text>
234
+ <Text
235
+ color={contextColor(props.contextUsage.pressure)}
236
+ dimColor={props.contextUsage.pressure === "normal"}
237
+ >
238
+ {formatContextUsageLine(props.contextUsage)}
239
+ </Text>
240
+ </>
241
+ )}
242
+ </Box>
243
+ )}
244
+ {showSuggestions ? (
245
+ <Box flexDirection="column">
246
+ {suggestions.map((command, index) => (
247
+ <Text key={command.name}>
248
+ {index === selectedIndex ? "❯ " : " "}/{command.name}
249
+ <Text dimColor> {command.description}</Text>
250
+ </Text>
251
+ ))}
252
+ </Box>
253
+ ) : null}
254
+ </Box>
255
+ );
256
+ }
257
+
258
+ function contextColor(
259
+ pressure: ContextUsageSnapshot["pressure"],
260
+ ): "yellow" | "red" | undefined {
261
+ if (pressure === "blocked") {
262
+ return "red";
263
+ }
264
+ return pressure === "triggered" ? "yellow" : undefined;
265
+ }
266
+
267
+ function formatWorkspacePath(workspaceRoot: string): string {
268
+ const home = os.homedir();
269
+ const relative = path.relative(home, workspaceRoot);
270
+
271
+ if (relative === "") {
272
+ return home;
273
+ }
274
+
275
+ const isInsideHome =
276
+ relative !== ".." &&
277
+ !relative.startsWith(`..${path.sep}`) &&
278
+ !path.isAbsolute(relative);
279
+
280
+ return isInsideHome ? `~${path.sep}${relative}` : workspaceRoot;
281
+ }
282
+
283
+ function renderEditor(editor: LineEditorState, props: PromptInputProps) {
284
+ if (editor.value === "") {
285
+ const placeholder = props.placeholder ?? "";
286
+
287
+ if (props.isDisabled === true) {
288
+ return <Text dimColor>{placeholder}</Text>;
289
+ }
290
+
291
+ if (placeholder === "") {
292
+ return <Text inverse> </Text>;
293
+ }
294
+
295
+ return (
296
+ <Text>
297
+ <Text inverse>{placeholder.slice(0, 1)}</Text>
298
+ <Text dimColor>{placeholder.slice(1)}</Text>
299
+ </Text>
300
+ );
301
+ }
302
+
303
+ if (props.isDisabled === true) {
304
+ return <Text>{editor.value}</Text>;
305
+ }
306
+
307
+ const { before, at, after } = splitAtCursor(editor);
308
+ if (at === "\n") {
309
+ return (
310
+ <Text>
311
+ {before}
312
+ <Text inverse> </Text>
313
+ {"\n"}
314
+ {after}
315
+ </Text>
316
+ );
317
+ }
318
+
319
+ return (
320
+ <Text>
321
+ {before}
322
+ <Text inverse>{at}</Text>
323
+ {after}
324
+ </Text>
325
+ );
326
+ }
327
+
328
+ function normalizeLineBreaks(value: string): string {
329
+ return value.replace(/\r\n?/g, "\n");
330
+ }
331
+
332
+ function editorChanged(before: LineEditorState, after: LineEditorState): boolean {
333
+ return before.value !== after.value || before.cursor !== after.cursor;
334
+ }
335
+
336
+ function applyEditorChange(
337
+ state: PromptInputState,
338
+ editor: LineEditorState,
339
+ ): PromptInputState {
340
+ const valueChanged = state.editor.value !== editor.value;
341
+ let navigation = state.navigation;
342
+
343
+ if (valueChanged) {
344
+ navigation = undefined;
345
+ } else if (navigation !== undefined) {
346
+ navigation = { ...navigation, browsing: false };
347
+ }
348
+
349
+ return {
350
+ ...state,
351
+ editor,
352
+ navigation,
353
+ suggestionIndex: valueChanged ? 0 : state.suggestionIndex,
354
+ suggestionsDismissed: valueChanged ? false : state.suggestionsDismissed,
355
+ };
356
+ }
357
+
358
+ function navigateHistoryUp(
359
+ state: PromptInputState,
360
+ entries: readonly string[],
361
+ ): PromptInputState {
362
+ if (entries.length === 0) {
363
+ return state;
364
+ }
365
+
366
+ if (state.navigation === undefined) {
367
+ const index = entries.length - 1;
368
+ return showHistoryEntry(state, entries, {
369
+ index,
370
+ originalDraft: state.editor,
371
+ browsing: true,
372
+ });
373
+ }
374
+
375
+ if (state.navigation.index === 0) {
376
+ return {
377
+ ...state,
378
+ navigation: { ...state.navigation, browsing: true },
379
+ };
380
+ }
381
+
382
+ const navigation = {
383
+ ...state.navigation,
384
+ index: state.navigation.index - 1,
385
+ browsing: true,
386
+ };
387
+ return showHistoryEntry(state, entries, navigation);
388
+ }
389
+
390
+ function navigateHistoryDown(
391
+ state: PromptInputState,
392
+ entries: readonly string[],
393
+ ): PromptInputState {
394
+ const navigation = state.navigation;
395
+ if (navigation === undefined) {
396
+ return state;
397
+ }
398
+
399
+ if (navigation.index >= entries.length - 1) {
400
+ return {
401
+ editor: navigation.originalDraft,
402
+ suggestionIndex: 0,
403
+ suggestionsDismissed: false,
404
+ };
405
+ }
406
+
407
+ const nextNavigation = {
408
+ ...navigation,
409
+ index: navigation.index + 1,
410
+ browsing: true,
411
+ };
412
+ return showHistoryEntry(state, entries, nextNavigation);
413
+ }
414
+
415
+ function showHistoryEntry(
416
+ state: PromptInputState,
417
+ entries: readonly string[],
418
+ navigation: HistoryNavigation,
419
+ ): PromptInputState {
420
+ const entry = entries[navigation.index];
421
+ if (entry === undefined) {
422
+ throw new Error(`History entry ${navigation.index} does not exist`);
423
+ }
424
+
425
+ return {
426
+ ...state,
427
+ editor: createLineEditorState(entry),
428
+ navigation,
429
+ suggestionIndex: 0,
430
+ suggestionsDismissed: true,
431
+ };
432
+ }