hoomanjs 1.12.0 → 1.13.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.
@@ -0,0 +1,76 @@
1
+ import React from "react";
2
+ import { Box, Text, useStdout } from "ink";
3
+ import type { PromptSubmission } from "./prompt-input/usePromptInputController.ts";
4
+
5
+ type QueuedPromptsProps = {
6
+ prompts: readonly { id: string; prompt: PromptSubmission }[];
7
+ };
8
+
9
+ const MIN_PROMPT_PREVIEW_CHARS = 16;
10
+ const MAX_PROMPT_PREVIEW_CHARS = 120;
11
+ const ELLIPSIS = "...";
12
+
13
+ function normalizePrompt(prompt: string): string {
14
+ return prompt.replace(/\s+/g, " ").trim();
15
+ }
16
+
17
+ function promptPreview(prompt: PromptSubmission): string {
18
+ const text = normalizePrompt(prompt.text);
19
+ if (prompt.attachments.length === 0) {
20
+ return text;
21
+ }
22
+ const suffix = `${prompt.attachments.length} attachment${
23
+ prompt.attachments.length === 1 ? "" : "s"
24
+ }`;
25
+ return text ? `${text} (${suffix})` : suffix;
26
+ }
27
+
28
+ function truncatePrompt(prompt: string, maxChars: number): string {
29
+ if (prompt.length <= maxChars) {
30
+ return prompt;
31
+ }
32
+ if (maxChars <= ELLIPSIS.length) {
33
+ return ELLIPSIS.slice(0, maxChars);
34
+ }
35
+ return `${prompt.slice(0, maxChars - ELLIPSIS.length)}${ELLIPSIS}`;
36
+ }
37
+
38
+ export function QueuedPrompts({
39
+ prompts,
40
+ }: QueuedPromptsProps): React.JSX.Element | null {
41
+ const { stdout } = useStdout();
42
+ if (prompts.length === 0) {
43
+ return null;
44
+ }
45
+
46
+ const columns = stdout?.columns ?? 80;
47
+ const maxPromptChars = Math.max(
48
+ MIN_PROMPT_PREVIEW_CHARS,
49
+ Math.min(MAX_PROMPT_PREVIEW_CHARS, columns - 8),
50
+ );
51
+
52
+ return (
53
+ <Box
54
+ flexDirection="column"
55
+ borderStyle="round"
56
+ borderColor="gray"
57
+ paddingX={1}
58
+ >
59
+ <Text color="gray">
60
+ queued {prompts.length === 1 ? "prompt" : "prompts"}
61
+ </Text>
62
+ {prompts.map((item) => {
63
+ const preview = truncatePrompt(
64
+ promptPreview(item.prompt),
65
+ maxPromptChars,
66
+ );
67
+ return (
68
+ <Text key={item.id} color="gray">
69
+ {"\u25cb "}
70
+ {preview}
71
+ </Text>
72
+ );
73
+ })}
74
+ </Box>
75
+ );
76
+ }
@@ -4,6 +4,7 @@ import type { Manager as McpManager } from "../../core/mcp/index.ts";
4
4
  type StatusBarProps = {
5
5
  running: boolean;
6
6
  status: string;
7
+ statusLabel?: string;
7
8
  sessionId: string;
8
9
  elapsedLabel: string;
9
10
  turnCount: number;
@@ -36,6 +37,7 @@ function statusValueColor(status: string): string {
36
37
  export function StatusBar({
37
38
  running,
38
39
  status,
40
+ statusLabel,
39
41
  sessionId,
40
42
  elapsedLabel,
41
43
  turnCount,
@@ -48,7 +50,7 @@ export function StatusBar({
48
50
  <Box marginTop={1} flexDirection="column">
49
51
  <Text>
50
52
  <Text color="gray">status: </Text>
51
- <Text color={statusValueColor(status)}>{status}</Text>
53
+ <Text color={statusValueColor(status)}>{statusLabel ?? status}</Text>
52
54
  <Text color="gray"> • session: {sessionId}</Text>
53
55
  </Text>
54
56
  <Text color="gray">
@@ -0,0 +1,49 @@
1
+ import { Box, Text } from "ink";
2
+ import type { TodoItem } from "../../core/tools/todo.ts";
3
+
4
+ type TodoPanelProps = {
5
+ todos: TodoItem[];
6
+ };
7
+
8
+ function markerForStatus(status: TodoItem["status"]): string {
9
+ switch (status) {
10
+ case "completed":
11
+ return "[x]";
12
+ case "in_progress":
13
+ return "[~]";
14
+ case "pending":
15
+ return "[ ]";
16
+ }
17
+ }
18
+
19
+ export function TodoPanel({ todos }: TodoPanelProps) {
20
+ if (todos.length === 0) {
21
+ return null;
22
+ }
23
+
24
+ return (
25
+ <Box flexDirection="column" marginTop={1}>
26
+ <Text bold color="cyan">
27
+ Todos
28
+ </Text>
29
+ {todos.map((todo, index) => {
30
+ const completed = todo.status === "completed";
31
+ const inProgress = todo.status === "in_progress";
32
+ const marker = markerForStatus(todo.status);
33
+ const suffix =
34
+ inProgress && todo.activeForm.trim().length > 0
35
+ ? ` — ${todo.activeForm}`
36
+ : "";
37
+ return (
38
+ <Text
39
+ key={`${index}-${todo.content}`}
40
+ dimColor={completed}
41
+ bold={inProgress}
42
+ >
43
+ {`${index + 1}. ${marker} ${todo.content}${suffix}`}
44
+ </Text>
45
+ );
46
+ })}
47
+ </Box>
48
+ );
49
+ }
@@ -0,0 +1,119 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { attachmentsPath } from "../../../core/utils/paths.ts";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ function appleScriptString(value: string): string {
11
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
12
+ }
13
+
14
+ async function createClipboardImagePath(): Promise<string> {
15
+ const root = attachmentsPath();
16
+ await mkdir(root, { recursive: true });
17
+ return join(root, `${randomUUID()}-clipboard.png`);
18
+ }
19
+
20
+ async function fileHasContent(path: string): Promise<boolean> {
21
+ try {
22
+ const data = await readFile(path);
23
+ return data.length > 0;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ async function saveMacClipboardImage(): Promise<string | null> {
30
+ const outputPath = await createClipboardImagePath();
31
+ const outputPathLiteral = appleScriptString(outputPath);
32
+
33
+ try {
34
+ await execFileAsync("osascript", [
35
+ "-e",
36
+ "set png_data to (the clipboard as «class PNGf»)",
37
+ "-e",
38
+ `set fp to open for access POSIX file ${outputPathLiteral} with write permission`,
39
+ "-e",
40
+ "set eof fp to 0",
41
+ "-e",
42
+ "write png_data to fp",
43
+ "-e",
44
+ "close access fp",
45
+ ]);
46
+ return (await fileHasContent(outputPath)) ? outputPath : null;
47
+ } catch {
48
+ await unlink(outputPath).catch(() => undefined);
49
+ return null;
50
+ }
51
+ }
52
+
53
+ async function saveLinuxClipboardImage(): Promise<string | null> {
54
+ const outputPath = await createClipboardImagePath();
55
+ const candidates: Array<[string, string[]]> = [
56
+ ["wl-paste", ["--type", "image/png"]],
57
+ ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]],
58
+ ["xclip", ["-selection", "clipboard", "-t", "image/jpeg", "-o"]],
59
+ ["xclip", ["-selection", "clipboard", "-t", "image/webp", "-o"]],
60
+ ["xsel", ["--clipboard", "--output"]],
61
+ ];
62
+
63
+ for (const [command, args] of candidates) {
64
+ try {
65
+ const { stdout } = await execFileAsync(command, args, {
66
+ encoding: "buffer",
67
+ maxBuffer: 20 * 1024 * 1024,
68
+ });
69
+ if (!Buffer.isBuffer(stdout) || stdout.length === 0) {
70
+ continue;
71
+ }
72
+ await writeFile(outputPath, stdout);
73
+ return outputPath;
74
+ } catch {
75
+ // Try the next clipboard utility.
76
+ }
77
+ }
78
+
79
+ await unlink(outputPath).catch(() => undefined);
80
+ return null;
81
+ }
82
+
83
+ async function saveWindowsClipboardImage(): Promise<string | null> {
84
+ const outputPath = await createClipboardImagePath();
85
+ const escapedPath = outputPath.replace(/'/g, "''");
86
+ const script = [
87
+ "Add-Type -AssemblyName System.Windows.Forms",
88
+ "Add-Type -AssemblyName System.Drawing",
89
+ "$img = [System.Windows.Forms.Clipboard]::GetImage()",
90
+ "if ($null -eq $img) { exit 1 }",
91
+ `$img.Save('${escapedPath}', [System.Drawing.Imaging.ImageFormat]::Png)`,
92
+ ].join("; ");
93
+
94
+ try {
95
+ await execFileAsync("powershell.exe", [
96
+ "-NoProfile",
97
+ "-NonInteractive",
98
+ "-Command",
99
+ script,
100
+ ]);
101
+ return (await fileHasContent(outputPath)) ? outputPath : null;
102
+ } catch {
103
+ await unlink(outputPath).catch(() => undefined);
104
+ return null;
105
+ }
106
+ }
107
+
108
+ export async function saveClipboardImageAsAttachment(): Promise<string | null> {
109
+ switch (process.platform) {
110
+ case "darwin":
111
+ return saveMacClipboardImage();
112
+ case "linux":
113
+ return saveLinuxClipboardImage();
114
+ case "win32":
115
+ return saveWindowsClipboardImage();
116
+ default:
117
+ return null;
118
+ }
119
+ }
@@ -0,0 +1,294 @@
1
+ export type InputState = {
2
+ value: string;
3
+ cursor: number;
4
+ };
5
+
6
+ const TOKEN_AT_END = /(^|\s)\[(?:paste|attachment) #\d+\]$/;
7
+ const WORD_CHAR = /[\p{L}\p{N}_]/u;
8
+
9
+ const graphemeSegmenter =
10
+ typeof Intl !== "undefined" && "Segmenter" in Intl
11
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
12
+ : null;
13
+
14
+ export function clampCursor(value: string, cursor: number): number {
15
+ const n = Number.isFinite(cursor) ? Math.trunc(cursor) : 0;
16
+ if (n < 0) {
17
+ return 0;
18
+ }
19
+ if (n > value.length) {
20
+ return value.length;
21
+ }
22
+ return n;
23
+ }
24
+
25
+ function replaceRange(
26
+ value: string,
27
+ start: number,
28
+ end: number,
29
+ replacement: string,
30
+ ): string {
31
+ return value.slice(0, start) + replacement + value.slice(end);
32
+ }
33
+
34
+ function deleteRange(
35
+ state: InputState,
36
+ start: number,
37
+ end: number,
38
+ ): InputState {
39
+ const safeStart = clampCursor(state.value, start);
40
+ const safeEnd = clampCursor(state.value, end);
41
+ return {
42
+ value: replaceRange(state.value, safeStart, safeEnd, ""),
43
+ cursor: safeStart,
44
+ };
45
+ }
46
+
47
+ function isWhitespace(char: string): boolean {
48
+ return /\s/.test(char);
49
+ }
50
+
51
+ function isWordChar(char: string): boolean {
52
+ return WORD_CHAR.test(char);
53
+ }
54
+
55
+ function moveToPrevGrapheme(text: string, at: number): number {
56
+ const n = clampCursor(text, at);
57
+ if (n <= 0) {
58
+ return 0;
59
+ }
60
+ if (!graphemeSegmenter) {
61
+ const cp = text.codePointAt(n - 1) ?? 0;
62
+ return n - (cp > 0xffff ? 2 : 1);
63
+ }
64
+ let last = 0;
65
+ for (const seg of graphemeSegmenter.segment(text)) {
66
+ if (seg.index >= n) {
67
+ break;
68
+ }
69
+ last = seg.index;
70
+ }
71
+ return last;
72
+ }
73
+
74
+ function moveToNextGrapheme(text: string, at: number): number {
75
+ const n = clampCursor(text, at);
76
+ if (n >= text.length) {
77
+ return text.length;
78
+ }
79
+ if (!graphemeSegmenter) {
80
+ const cp = text.codePointAt(n) ?? 0;
81
+ return n + (cp > 0xffff ? 2 : 1);
82
+ }
83
+ for (const seg of graphemeSegmenter.segment(text)) {
84
+ if (seg.index > n) {
85
+ return seg.index;
86
+ }
87
+ }
88
+ return text.length;
89
+ }
90
+
91
+ function graphemeLeftOfCursor(text: string, at: number): string {
92
+ const p = moveToPrevGrapheme(text, at);
93
+ const n = clampCursor(text, at);
94
+ return text.slice(p, n);
95
+ }
96
+
97
+ function graphemeRightOfCursor(text: string, at: number): string {
98
+ const n = clampCursor(text, at);
99
+ const x = moveToNextGrapheme(text, n);
100
+ return text.slice(n, x);
101
+ }
102
+
103
+ type CharKind = "space" | "word" | "other";
104
+
105
+ function getCharKind(char: string): CharKind {
106
+ if (isWhitespace(char)) {
107
+ return "space";
108
+ }
109
+ if (isWordChar(char)) {
110
+ return "word";
111
+ }
112
+ return "other";
113
+ }
114
+
115
+ export function insertText(state: InputState, text: string): InputState {
116
+ return {
117
+ value: replaceRange(state.value, state.cursor, state.cursor, text),
118
+ cursor: state.cursor + text.length,
119
+ };
120
+ }
121
+
122
+ export function moveCursorLeft(state: InputState): InputState {
123
+ return {
124
+ ...state,
125
+ cursor: moveToPrevGrapheme(state.value, state.cursor),
126
+ };
127
+ }
128
+
129
+ export function moveCursorRight(state: InputState): InputState {
130
+ return {
131
+ ...state,
132
+ cursor: moveToNextGrapheme(state.value, state.cursor),
133
+ };
134
+ }
135
+
136
+ export function moveCursorWordLeft(state: InputState): InputState {
137
+ let next = state.cursor;
138
+ while (
139
+ next > 0 &&
140
+ getCharKind(graphemeLeftOfCursor(state.value, next)) === "space"
141
+ ) {
142
+ next = moveToPrevGrapheme(state.value, next);
143
+ }
144
+ if (next === 0) {
145
+ return { ...state, cursor: 0 };
146
+ }
147
+
148
+ const kind = getCharKind(graphemeLeftOfCursor(state.value, next));
149
+ while (
150
+ next > 0 &&
151
+ getCharKind(graphemeLeftOfCursor(state.value, next)) === kind
152
+ ) {
153
+ next = moveToPrevGrapheme(state.value, next);
154
+ }
155
+ return { ...state, cursor: next };
156
+ }
157
+
158
+ export function moveCursorWordRight(state: InputState): InputState {
159
+ const { value } = state;
160
+ let next = state.cursor;
161
+ while (
162
+ next < value.length &&
163
+ getCharKind(graphemeRightOfCursor(value, next)) === "space"
164
+ ) {
165
+ next = moveToNextGrapheme(value, next);
166
+ }
167
+ if (next >= value.length) {
168
+ return { ...state, cursor: value.length };
169
+ }
170
+
171
+ const kind = getCharKind(graphemeRightOfCursor(value, next));
172
+ while (
173
+ next < value.length &&
174
+ getCharKind(graphemeRightOfCursor(value, next)) === kind
175
+ ) {
176
+ next = moveToNextGrapheme(value, next);
177
+ }
178
+ return { ...state, cursor: next };
179
+ }
180
+
181
+ export function findLineStart(value: string, cursor: number): number {
182
+ const index = value.lastIndexOf("\n", Math.max(0, cursor - 1));
183
+ return index === -1 ? 0 : index + 1;
184
+ }
185
+
186
+ export function findLineEnd(value: string, cursor: number): number {
187
+ const index = value.indexOf("\n", cursor);
188
+ return index === -1 ? value.length : index;
189
+ }
190
+
191
+ export function moveCursorLineStart(state: InputState): InputState {
192
+ return { ...state, cursor: findLineStart(state.value, state.cursor) };
193
+ }
194
+
195
+ export function moveCursorLineEnd(state: InputState): InputState {
196
+ return { ...state, cursor: findLineEnd(state.value, state.cursor) };
197
+ }
198
+
199
+ export function moveCursorUp(
200
+ state: InputState,
201
+ targetColumn?: number,
202
+ ): { state: InputState; targetColumn: number } {
203
+ const currentStart = findLineStart(state.value, state.cursor);
204
+ if (currentStart === 0) {
205
+ return { state, targetColumn: targetColumn ?? state.cursor };
206
+ }
207
+ const previousLineEnd = currentStart - 1;
208
+ const previousLineStart = findLineStart(state.value, previousLineEnd);
209
+ const currentColumn = state.cursor - currentStart;
210
+ const preferred = targetColumn ?? currentColumn;
211
+ const previousLength = previousLineEnd - previousLineStart;
212
+ const nextCursor = previousLineStart + Math.min(preferred, previousLength);
213
+ return { state: { ...state, cursor: nextCursor }, targetColumn: preferred };
214
+ }
215
+
216
+ export function moveCursorDown(
217
+ state: InputState,
218
+ targetColumn?: number,
219
+ ): { state: InputState; targetColumn: number } {
220
+ const currentEnd = findLineEnd(state.value, state.cursor);
221
+ if (currentEnd >= state.value.length) {
222
+ const currentStart = findLineStart(state.value, state.cursor);
223
+ return {
224
+ state,
225
+ targetColumn: targetColumn ?? state.cursor - currentStart,
226
+ };
227
+ }
228
+ const nextLineStart = currentEnd + 1;
229
+ const nextLineEnd = findLineEnd(state.value, nextLineStart);
230
+ const currentStart = findLineStart(state.value, state.cursor);
231
+ const currentColumn = state.cursor - currentStart;
232
+ const preferred = targetColumn ?? currentColumn;
233
+ const nextLength = nextLineEnd - nextLineStart;
234
+ const nextCursor = nextLineStart + Math.min(preferred, nextLength);
235
+ return { state: { ...state, cursor: nextCursor }, targetColumn: preferred };
236
+ }
237
+
238
+ function findPasteTokenStart(value: string, cursor: number): number | null {
239
+ if (cursor === 0) {
240
+ return null;
241
+ }
242
+ const charAfter = value[cursor];
243
+ if (charAfter !== undefined && !isWhitespace(charAfter)) {
244
+ return null;
245
+ }
246
+ const before = value.slice(0, cursor);
247
+ const match = before.match(TOKEN_AT_END);
248
+ if (!match || match.index === undefined) {
249
+ return null;
250
+ }
251
+ return match.index + (match[1] ?? "").length;
252
+ }
253
+
254
+ export function deleteBackward(state: InputState): InputState {
255
+ if (state.cursor === 0) {
256
+ return state;
257
+ }
258
+
259
+ const tokenStart = findPasteTokenStart(state.value, state.cursor);
260
+ if (tokenStart !== null) {
261
+ return deleteRange(state, tokenStart, state.cursor);
262
+ }
263
+
264
+ const from = moveToPrevGrapheme(state.value, state.cursor);
265
+ return deleteRange(state, from, state.cursor);
266
+ }
267
+
268
+ export function deleteForward(state: InputState): InputState {
269
+ if (state.cursor >= state.value.length) {
270
+ return state;
271
+ }
272
+ const to = moveToNextGrapheme(state.value, state.cursor);
273
+ return deleteRange(state, state.cursor, to);
274
+ }
275
+
276
+ export function deleteWordBackward(state: InputState): InputState {
277
+ const moved = moveCursorWordLeft(state);
278
+ return deleteRange(state, moved.cursor, state.cursor);
279
+ }
280
+
281
+ export function deleteWordForward(state: InputState): InputState {
282
+ const moved = moveCursorWordRight(state);
283
+ return deleteRange(state, state.cursor, moved.cursor);
284
+ }
285
+
286
+ export function deleteToLineStart(state: InputState): InputState {
287
+ const lineStart = findLineStart(state.value, state.cursor);
288
+ return deleteRange(state, lineStart, state.cursor);
289
+ }
290
+
291
+ export function deleteToLineEnd(state: InputState): InputState {
292
+ const lineEnd = findLineEnd(state.value, state.cursor);
293
+ return deleteRange(state, state.cursor, lineEnd);
294
+ }
@@ -0,0 +1,105 @@
1
+ export const PASTE_THRESHOLD_CHARS = 800;
2
+ export const PASTE_THRESHOLD_LINE_BREAKS = 2;
3
+
4
+ const ANSI_ESCAPE_PATTERN =
5
+ /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
6
+ const PASTE_REF_PATTERN = /\[paste #(\d+)\]/g;
7
+ const ATTACHMENT_REF_PATTERN = /\[attachment #(\d+)\]/g;
8
+
9
+ function toPasteId(raw: string | undefined): number | null {
10
+ if (!raw) {
11
+ return null;
12
+ }
13
+ const id = Number.parseInt(raw, 10);
14
+ return Number.isFinite(id) && id > 0 ? id : null;
15
+ }
16
+
17
+ export function normalizePastedText(input: string): string {
18
+ return input
19
+ .replace(ANSI_ESCAPE_PATTERN, "")
20
+ .replace(/\r\n/g, "\n")
21
+ .replace(/\r/g, "\n")
22
+ .replaceAll("\t", " ");
23
+ }
24
+
25
+ export function countLineBreaks(text: string): number {
26
+ let count = 0;
27
+ for (const ch of text) {
28
+ if (ch === "\n") {
29
+ count += 1;
30
+ }
31
+ }
32
+ return count;
33
+ }
34
+
35
+ export function formatPasteRef(id: number): string {
36
+ return `[paste #${id}]`;
37
+ }
38
+
39
+ export function formatAttachmentRef(id: number): string {
40
+ return `[attachment #${id}]`;
41
+ }
42
+
43
+ export function parsePasteRefs(input: string): number[] {
44
+ const ids: number[] = [];
45
+ for (const match of input.matchAll(PASTE_REF_PATTERN)) {
46
+ const id = toPasteId(match[1]);
47
+ if (id !== null) {
48
+ ids.push(id);
49
+ }
50
+ }
51
+ return ids;
52
+ }
53
+
54
+ export function parseAttachmentRefs(input: string): number[] {
55
+ const ids: number[] = [];
56
+ for (const match of input.matchAll(ATTACHMENT_REF_PATTERN)) {
57
+ const id = toPasteId(match[1]);
58
+ if (id !== null) {
59
+ ids.push(id);
60
+ }
61
+ }
62
+ return ids;
63
+ }
64
+
65
+ export function shouldCollapsePaste(
66
+ text: string,
67
+ maxChars = PASTE_THRESHOLD_CHARS,
68
+ maxLineBreaks = PASTE_THRESHOLD_LINE_BREAKS,
69
+ ): boolean {
70
+ return text.length > maxChars || countLineBreaks(text) > maxLineBreaks;
71
+ }
72
+
73
+ export function expandPasteRefs(
74
+ input: string,
75
+ pastedContents: Readonly<Record<number, string>>,
76
+ ): string {
77
+ return input.replace(PASTE_REF_PATTERN, (match, rawId: string) => {
78
+ const id = toPasteId(rawId);
79
+ if (id === null) {
80
+ return match;
81
+ }
82
+ return pastedContents[id] ?? match;
83
+ });
84
+ }
85
+
86
+ function removeOuterQuotes(text: string): string {
87
+ return text.replace(/^["']+/, "").replace(/["']+$/, "");
88
+ }
89
+
90
+ function stripBackslashEscapes(text: string): string {
91
+ if (process.platform === "win32") {
92
+ return text;
93
+ }
94
+ return text.replace(/\\(.)/g, "$1");
95
+ }
96
+
97
+ export function parsePastedFilePathCandidates(input: string): string[] {
98
+ const text = normalizePastedText(input);
99
+ const parts = text
100
+ .split(/ (?=\/|~\/|\.{1,2}\/|[A-Za-z]:\\)/)
101
+ .flatMap((part) => part.split("\n"))
102
+ .map((part) => stripBackslashEscapes(removeOuterQuotes(part.trim())))
103
+ .filter(Boolean);
104
+ return [...new Set(parts)];
105
+ }