@abacus-ai/cli 2.0.0-canary.1 → 2.0.0-canary.3

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 (197) hide show
  1. package/dist/index.mjs +450 -422
  2. package/package.json +4 -1
  3. package/.oxlintrc.json +0 -8
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +0 -9
  6. package/src/__e2e__/README.md +0 -196
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
  10. package/src/__e2e__/conversation.e2e.test.tsx +0 -56
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
  12. package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
  13. package/src/__e2e__/helpers/test-helpers.ts +0 -449
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
  15. package/src/__e2e__/llm-models.e2e.test.ts +0 -402
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
  19. package/src/__e2e__/repl.e2e.test.tsx +0 -78
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
  23. package/src/args.ts +0 -22
  24. package/src/components/__tests__/react-compiler.test.tsx +0 -78
  25. package/src/components/__tests__/status-indicator.test.tsx +0 -403
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
  27. package/src/components/composer/agent-mode-indicator.tsx +0 -63
  28. package/src/components/composer/bash-runner.tsx +0 -54
  29. package/src/components/composer/commands/default-commands.tsx +0 -615
  30. package/src/components/composer/commands/handler.tsx +0 -59
  31. package/src/components/composer/commands/picker.tsx +0 -273
  32. package/src/components/composer/commands/registry.ts +0 -233
  33. package/src/components/composer/commands/types.ts +0 -33
  34. package/src/components/composer/context.tsx +0 -88
  35. package/src/components/composer/file-mention-picker.tsx +0 -83
  36. package/src/components/composer/help.tsx +0 -44
  37. package/src/components/composer/index.tsx +0 -1007
  38. package/src/components/composer/mentions.ts +0 -57
  39. package/src/components/composer/message-queue.tsx +0 -70
  40. package/src/components/composer/mode-panel.tsx +0 -35
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
  43. package/src/components/composer/modes/bash-handler.tsx +0 -132
  44. package/src/components/composer/modes/bash-renderer.tsx +0 -175
  45. package/src/components/composer/modes/default-handlers.tsx +0 -33
  46. package/src/components/composer/modes/index.ts +0 -41
  47. package/src/components/composer/modes/types.ts +0 -21
  48. package/src/components/composer/persistent-shell.ts +0 -283
  49. package/src/components/composer/process.ts +0 -65
  50. package/src/components/composer/types.ts +0 -9
  51. package/src/components/composer/use-mention-search.ts +0 -68
  52. package/src/components/error-boundry.tsx +0 -60
  53. package/src/components/exit-message.tsx +0 -29
  54. package/src/components/expanded-view.tsx +0 -74
  55. package/src/components/file-completion.tsx +0 -127
  56. package/src/components/header.tsx +0 -47
  57. package/src/components/logo.tsx +0 -37
  58. package/src/components/segments.tsx +0 -356
  59. package/src/components/status-indicator.tsx +0 -306
  60. package/src/components/tool-group-summary.tsx +0 -263
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -319
  62. package/src/components/tool-permissions/diff-preview.tsx +0 -359
  63. package/src/components/tool-permissions/index.ts +0 -5
  64. package/src/components/tool-permissions/permission-options.tsx +0 -401
  65. package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +0 -420
  67. package/src/components/tools/agent/ask-user-question.tsx +0 -107
  68. package/src/components/tools/agent/enter-plan-mode.tsx +0 -55
  69. package/src/components/tools/agent/exit-plan-mode.tsx +0 -83
  70. package/src/components/tools/agent/handoff-to-main.tsx +0 -27
  71. package/src/components/tools/agent/subagent.tsx +0 -37
  72. package/src/components/tools/agent/todo-write.tsx +0 -104
  73. package/src/components/tools/browser/close-tab.tsx +0 -58
  74. package/src/components/tools/browser/computer.tsx +0 -70
  75. package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
  76. package/src/components/tools/browser/get-tab-content.tsx +0 -51
  77. package/src/components/tools/browser/navigate-to.tsx +0 -59
  78. package/src/components/tools/browser/new-tab.tsx +0 -60
  79. package/src/components/tools/browser/perform-action.tsx +0 -63
  80. package/src/components/tools/browser/refresh-tab.tsx +0 -43
  81. package/src/components/tools/browser/switch-tab.tsx +0 -58
  82. package/src/components/tools/filesystem/delete-file.tsx +0 -104
  83. package/src/components/tools/filesystem/edit.tsx +0 -220
  84. package/src/components/tools/filesystem/list-dir.tsx +0 -78
  85. package/src/components/tools/filesystem/read-file.tsx +0 -180
  86. package/src/components/tools/filesystem/upload-image.tsx +0 -76
  87. package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
  88. package/src/components/tools/index.ts +0 -91
  89. package/src/components/tools/mcp/mcp-tool.tsx +0 -158
  90. package/src/components/tools/search/fetch-url.tsx +0 -73
  91. package/src/components/tools/search/file-search.tsx +0 -78
  92. package/src/components/tools/search/grep.tsx +0 -90
  93. package/src/components/tools/search/semantic-search.tsx +0 -66
  94. package/src/components/tools/search/web-search.tsx +0 -71
  95. package/src/components/tools/shared/index.tsx +0 -48
  96. package/src/components/tools/shared/zod-coercion.ts +0 -35
  97. package/src/components/tools/terminal/bash-tool-output.tsx +0 -188
  98. package/src/components/tools/terminal/get-terminal-output.tsx +0 -91
  99. package/src/components/tools/terminal/run-in-terminal.tsx +0 -131
  100. package/src/components/tools/types.ts +0 -16
  101. package/src/components/tools.tsx +0 -68
  102. package/src/components/ui/__tests__/divider.test.tsx +0 -61
  103. package/src/components/ui/__tests__/gradient.test.tsx +0 -125
  104. package/src/components/ui/__tests__/input.test.tsx +0 -166
  105. package/src/components/ui/__tests__/select.test.tsx +0 -273
  106. package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
  107. package/src/components/ui/blinking-indicator.tsx +0 -27
  108. package/src/components/ui/divider.tsx +0 -162
  109. package/src/components/ui/gradient.tsx +0 -56
  110. package/src/components/ui/input.tsx +0 -228
  111. package/src/components/ui/select.tsx +0 -151
  112. package/src/components/ui/shimmer.tsx +0 -76
  113. package/src/context/agent-mode.tsx +0 -95
  114. package/src/context/extension-file.tsx +0 -136
  115. package/src/context/network-activity.tsx +0 -45
  116. package/src/context/notification.tsx +0 -62
  117. package/src/context/shell-size.tsx +0 -49
  118. package/src/context/shell-title.tsx +0 -38
  119. package/src/entrypoints/print-mode.ts +0 -312
  120. package/src/entrypoints/repl.tsx +0 -389
  121. package/src/hooks/use-agent.ts +0 -15
  122. package/src/hooks/use-api-client.ts +0 -1
  123. package/src/hooks/use-available-height.ts +0 -8
  124. package/src/hooks/use-cleanup.ts +0 -29
  125. package/src/hooks/use-interrupt-manager.ts +0 -242
  126. package/src/hooks/use-models.ts +0 -22
  127. package/src/index.ts +0 -217
  128. package/src/lib/__tests__/ansi.test.ts +0 -255
  129. package/src/lib/__tests__/cli.test.ts +0 -122
  130. package/src/lib/__tests__/commands.test.ts +0 -325
  131. package/src/lib/__tests__/constants.test.ts +0 -15
  132. package/src/lib/__tests__/focusables.test.ts +0 -25
  133. package/src/lib/__tests__/fs.test.ts +0 -231
  134. package/src/lib/__tests__/markdown.test.tsx +0 -348
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
  136. package/src/lib/__tests__/mcpManagement.test.ts +0 -38
  137. package/src/lib/__tests__/path-paste.test.ts +0 -144
  138. package/src/lib/__tests__/path.test.ts +0 -300
  139. package/src/lib/__tests__/queries.test.ts +0 -39
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
  141. package/src/lib/__tests__/text-buffer.test.ts +0 -328
  142. package/src/lib/__tests__/text-utils.test.ts +0 -32
  143. package/src/lib/__tests__/timing.test.ts +0 -78
  144. package/src/lib/__tests__/utils.test.ts +0 -238
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
  146. package/src/lib/ansi.ts +0 -150
  147. package/src/lib/cli-push-server.ts +0 -112
  148. package/src/lib/cli.ts +0 -44
  149. package/src/lib/clipboard.ts +0 -226
  150. package/src/lib/command-utils.ts +0 -93
  151. package/src/lib/commands.ts +0 -270
  152. package/src/lib/constants.ts +0 -3
  153. package/src/lib/extension-connection.ts +0 -181
  154. package/src/lib/focusables.ts +0 -7
  155. package/src/lib/fs.ts +0 -533
  156. package/src/lib/markdown/code-block.tsx +0 -63
  157. package/src/lib/markdown/index.ts +0 -4
  158. package/src/lib/markdown/link.tsx +0 -19
  159. package/src/lib/markdown/markdown.tsx +0 -372
  160. package/src/lib/markdown/types.ts +0 -15
  161. package/src/lib/mcpCommandHandler.ts +0 -121
  162. package/src/lib/mcpManagement.ts +0 -44
  163. package/src/lib/path-paste.ts +0 -185
  164. package/src/lib/path.ts +0 -179
  165. package/src/lib/queries.ts +0 -15
  166. package/src/lib/standaloneMcpService.ts +0 -688
  167. package/src/lib/status-utils.ts +0 -237
  168. package/src/lib/test-utils.tsx +0 -72
  169. package/src/lib/text-buffer.ts +0 -2415
  170. package/src/lib/text-utils.ts +0 -272
  171. package/src/lib/timing.ts +0 -63
  172. package/src/lib/types.ts +0 -295
  173. package/src/lib/utils.ts +0 -182
  174. package/src/lib/vim-buffer-actions.ts +0 -732
  175. package/src/providers/agent.tsx +0 -1063
  176. package/src/providers/api-client.tsx +0 -43
  177. package/src/services/logger.ts +0 -85
  178. package/src/terminal/detection.ts +0 -187
  179. package/src/terminal/exit.ts +0 -279
  180. package/src/terminal/notification.ts +0 -83
  181. package/src/terminal/progress.ts +0 -201
  182. package/src/terminal/setup.ts +0 -797
  183. package/src/terminal/types.ts +0 -51
  184. package/src/theme/context.tsx +0 -57
  185. package/src/theme/index.ts +0 -4
  186. package/src/theme/themed.tsx +0 -35
  187. package/src/theme/themes.json +0 -546
  188. package/src/theme/types.ts +0 -110
  189. package/src/tools/types.ts +0 -59
  190. package/src/tools/utils/__tests__/zod-coercion.test.ts +0 -33
  191. package/src/tools/utils/tool-ui-components.tsx +0 -649
  192. package/src/tools/utils/zod-coercion.ts +0 -35
  193. package/tsconfig.json +0 -16
  194. package/tsconfig.node.json +0 -29
  195. package/tsconfig.test.json +0 -27
  196. package/tsdown.config.ts +0 -17
  197. package/vitest.config.ts +0 -76
@@ -1,181 +0,0 @@
1
- import type { ContractRouterClient } from "@orpc/contract";
2
-
3
- import { createSocketClient, ideContract } from "@codellm/comms";
4
- import { product } from "@codellm/product";
5
- import { createORPCClient } from "@orpc/client";
6
- import * as fs from "fs";
7
- import * as os from "os";
8
- import * as path from "path";
9
-
10
- import type { CliPushServer } from "./cli-push-server.js";
11
-
12
- type IdeClient = ContractRouterClient<typeof ideContract>;
13
-
14
- /**
15
- * Client to connect manual CLI to the VS Code extension via oRPC over Unix socket.
16
- * Uses push-based communication — no polling.
17
- */
18
- export class ExtensionConnection {
19
- private client: IdeClient | null = null;
20
- private disposeSocket: (() => void) | null = null;
21
-
22
- private currentFile: string | undefined = undefined;
23
- private selectionStartLine: number | undefined = undefined;
24
- private selectionEndLine: number | undefined = undefined;
25
-
26
- private onFileChangeCallback:
27
- | ((file: string | undefined, startLine?: number, endLine?: number) => void)
28
- | null = null;
29
-
30
- constructor(private readonly pushServer: CliPushServer) {}
31
-
32
- private findSocketPath(): string | null {
33
- const envPath = process.env[product.socketEnvVar];
34
- if (envPath && fs.existsSync(envPath)) {
35
- return envPath;
36
- }
37
-
38
- // Fallback: scan the IDE socket dir for the most recently modified socket file
39
- // This handles the case where the terminal was opened before the extension set the env var
40
- try {
41
- const socketDir = path.join(os.homedir(), product.configDirName, product.ideDirName);
42
- const entries = fs.readdirSync(socketDir).filter((f) => f.endsWith(".sock"));
43
- if (entries.length === 0) return null;
44
-
45
- let best: { path: string; mtime: number } | null = null;
46
- for (const entry of entries) {
47
- const fullPath = path.join(socketDir, entry);
48
- try {
49
- const stat = fs.statSync(fullPath);
50
- if (!best || stat.mtimeMs > best.mtime) {
51
- best = { path: fullPath, mtime: stat.mtimeMs };
52
- }
53
- } catch {
54
- // stale entry
55
- }
56
- }
57
- if (best && fs.existsSync(best.path)) return best.path;
58
- } catch {
59
- // ignore
60
- }
61
- return null;
62
- }
63
-
64
- async connect(): Promise<boolean> {
65
- try {
66
- const socketPath = this.findSocketPath();
67
- if (!socketPath) {
68
- return false;
69
- }
70
-
71
- const handle = await createSocketClient(socketPath, { timeoutMs: 2000 });
72
- this.client = createORPCClient<IdeClient>(handle.link);
73
- this.disposeSocket = () => handle.dispose();
74
-
75
- // Register our push server with the extension
76
- await this.client.register({
77
- cliSocketPath: this.pushServer.socketPath,
78
- pid: process.pid,
79
- });
80
-
81
- // Seed state from push server (may already have state)
82
- const state = this.pushServer.getEditorState();
83
- this.currentFile = state.file;
84
- this.selectionStartLine = state.startLine;
85
- this.selectionEndLine = state.endLine;
86
-
87
- // Wire push-based updates
88
- this.pushServer.onEditorChange((s) => {
89
- this.currentFile = s.file;
90
- this.selectionStartLine = s.startLine;
91
- this.selectionEndLine = s.endLine;
92
- this.onFileChangeCallback?.(s.file, s.startLine, s.endLine);
93
- });
94
-
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- getCurrentFile(): string | undefined {
102
- return this.currentFile;
103
- }
104
-
105
- getSelectionRange(): { startLine?: number; endLine?: number } {
106
- return {
107
- startLine: this.selectionStartLine,
108
- endLine: this.selectionEndLine,
109
- };
110
- }
111
-
112
- isConnected(): boolean {
113
- return this.client !== null;
114
- }
115
-
116
- onFileChange(
117
- callback: (file: string | undefined, startLine?: number, endLine?: number) => void,
118
- ): void {
119
- this.onFileChangeCallback = callback;
120
- }
121
-
122
- async showDiff(params: {
123
- filePath: string;
124
- originalContent: string;
125
- modifiedContent: string;
126
- toolId: string;
127
- }): Promise<boolean> {
128
- if (!this.client) return false;
129
- try {
130
- const result = await this.client.showDiff({
131
- ...params,
132
- cliSocketPath: this.pushServer.socketPath,
133
- });
134
- return result.success;
135
- } catch {
136
- return false;
137
- }
138
- }
139
-
140
- async showToolView(params: { filePath: string; toolKey: string }): Promise<boolean> {
141
- if (!this.client) return false;
142
- try {
143
- const result = await this.client.showToolView({
144
- ...params,
145
- cliSocketPath: this.pushServer.socketPath,
146
- });
147
- return result.success;
148
- } catch {
149
- return false;
150
- }
151
- }
152
-
153
- async notifyDiffDecision(params: {
154
- targetFile: string;
155
- decision: "accept" | "reject";
156
- }): Promise<void> {
157
- if (!this.client) throw new Error("Not connected to extension");
158
- await this.client.notifyDiffDecision(params);
159
- }
160
-
161
- async openPlanFile(planFilePath: string): Promise<{ opened: boolean }> {
162
- if (!this.client) throw new Error("Not connected to extension");
163
- const result = await this.client.openFile({ file: planFilePath });
164
- return { opened: result.success };
165
- }
166
-
167
- async getDiagnostics(files?: string[]): Promise<unknown> {
168
- if (!this.client) throw new Error("Not connected to extension");
169
- return this.client.getDiagnostics({ ...(files && { files }) });
170
- }
171
-
172
- dispose(): void {
173
- this.disposeSocket?.();
174
- this.disposeSocket = null;
175
- this.client = null;
176
- this.currentFile = undefined;
177
- this.selectionStartLine = undefined;
178
- this.selectionEndLine = undefined;
179
- this.onFileChangeCallback = null;
180
- }
181
- }
@@ -1,7 +0,0 @@
1
- export enum Focusable {
2
- Composer = "composer",
3
- ModelSelector = "model-selector",
4
- Messages = "messages",
5
- Sidebar = "sidebar",
6
- ToolPermission = "tool-permission",
7
- }
package/src/lib/fs.ts DELETED
@@ -1,533 +0,0 @@
1
- import {
2
- getFileRecency,
3
- prepareQuery,
4
- scoreItemFuzzy,
5
- compareItemsByFuzzyScore,
6
- type IPreparedQuery,
7
- type IItemAccessor,
8
- type FuzzyScorerCache,
9
- LABEL_SCORE_THRESHOLD,
10
- } from "@codellm/agent/utils";
11
- import { rgPath } from "@vscode/ripgrep";
12
- import { spawn } from "node:child_process";
13
- import { promises as fs } from "node:fs";
14
- import path from "node:path";
15
- import { createInterface } from "node:readline";
16
-
17
- import { unescapePath } from "./path.js";
18
-
19
- class TtlCache<K, V> {
20
- private cache = new Map<K, { value: V; expiresAt: number }>();
21
- private ttlMs: number;
22
-
23
- constructor(ttlMs: number) {
24
- this.ttlMs = ttlMs;
25
- }
26
-
27
- get(key: K): V | undefined {
28
- const entry = this.cache.get(key);
29
- if (!entry) {
30
- return undefined;
31
- }
32
- if (Date.now() > entry.expiresAt) {
33
- this.cache.delete(key);
34
- return undefined;
35
- }
36
- return entry.value;
37
- }
38
-
39
- set(key: K, value: V): void {
40
- this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
41
- }
42
-
43
- clear(): void {
44
- this.cache.clear();
45
- }
46
-
47
- delete(key: K): void {
48
- this.cache.delete(key);
49
- }
50
- }
51
-
52
- type SearchOptions = {
53
- root: string;
54
- maxResults?: number;
55
- includeHidden?: boolean;
56
- globs?: string[];
57
- ignoreGlobs?: string[];
58
- useRipgrep?: boolean;
59
- useGitignore?: boolean;
60
- };
61
-
62
- type FileDoc = {
63
- path: string;
64
- name: string;
65
- dir: string;
66
- ext: string;
67
- isDirectory?: boolean;
68
- };
69
-
70
- type FileIndex = {
71
- docs: FileDoc[];
72
- building: boolean;
73
- builtAt?: number;
74
- buildError?: Error;
75
- };
76
-
77
- type SearchResult = {
78
- paths: string[];
79
- isDirectory: boolean[];
80
- };
81
-
82
- const SEARCH_RESULT_CACHE_TTL = 5 * 1000;
83
- const FILE_INDEX_CACHE_TTL = 60 * 1000;
84
-
85
- const searchResultCache = new TtlCache<string, SearchResult>(SEARCH_RESULT_CACHE_TTL);
86
- const fileIndexCache = new TtlCache<string, FileIndex>(FILE_INDEX_CACHE_TTL);
87
-
88
- const RECENCY_BOOST_SCORE = LABEL_SCORE_THRESHOLD / 4;
89
-
90
- function toDoc(filePath: string, isDirectory = false): FileDoc {
91
- const normalized = filePath.replace(/\\/g, "/");
92
- const name = path.posix.basename(normalized);
93
- const dir = path.posix.dirname(normalized);
94
- const ext = path.posix.extname(normalized);
95
- return { path: normalized, name, dir, ext, isDirectory };
96
- }
97
-
98
- function getIndexKey(root: string): string {
99
- return path.resolve(root).replace(/\\/g, "/");
100
- }
101
-
102
- function getResultCacheKey(
103
- query: string,
104
- options: Required<Pick<SearchOptions, "root" | "maxResults">>,
105
- ): string {
106
- return `${getIndexKey(options.root)}::${query}::${options.maxResults}`;
107
- }
108
-
109
- const fileDocAccessor: IItemAccessor<FileDoc> = {
110
- getItemLabel(item: FileDoc): string {
111
- return item.name;
112
- },
113
- getItemDescription(item: FileDoc): string {
114
- return item.dir;
115
- },
116
- getItemPath(item: FileDoc): string {
117
- return item.path;
118
- },
119
- };
120
-
121
- function searchAndRank(
122
- docs: ReadonlyArray<FileDoc>,
123
- query: IPreparedQuery,
124
- maxResults: number,
125
- ): SearchResult {
126
- if (docs.length === 0) {
127
- return { paths: [], isDirectory: [] };
128
- }
129
-
130
- const scorerCache: FuzzyScorerCache = {};
131
- const queryLower = query.normalizedLowercase;
132
- const queryLength = query.normalized.length;
133
-
134
- if (queryLength === 0) {
135
- return { paths: [], isDirectory: [] };
136
- }
137
-
138
- const scoredDocs: Array<{ doc: FileDoc; score: number; recencyBoost: number }> = [];
139
- const candidateLimit = Math.min(docs.length, maxResults * 15);
140
-
141
- const candidates: Array<{ doc: FileDoc; recency: number }> = [];
142
-
143
- for (const doc of docs) {
144
- const nameLower = doc.name.toLowerCase();
145
- const pathLower = doc.path.toLowerCase();
146
-
147
- const nameStarts = nameLower.startsWith(queryLower);
148
- const nameIncludes = nameLower.includes(queryLower);
149
- const pathIncludes = pathLower.includes(queryLower);
150
-
151
- if (nameStarts || nameIncludes || pathIncludes) {
152
- const recency = getFileRecency(doc.path);
153
- candidates.push({ doc, recency });
154
- }
155
- }
156
-
157
- candidates.sort((a, b) => b.recency - a.recency);
158
-
159
- let candidatesChecked = 0;
160
- for (const { doc, recency } of candidates) {
161
- if (candidatesChecked >= candidateLimit && scoredDocs.length >= maxResults * 2) {
162
- break;
163
- }
164
-
165
- candidatesChecked++;
166
-
167
- const recencyBoost = recency > 0 ? RECENCY_BOOST_SCORE : 0;
168
-
169
- const itemScore = scoreItemFuzzy(doc, query, true, fileDocAccessor, scorerCache);
170
- if (itemScore.score > 0) {
171
- scoredDocs.push({
172
- doc,
173
- score: itemScore.score + recencyBoost,
174
- recencyBoost,
175
- });
176
- }
177
- }
178
-
179
- scoredDocs.sort((a, b) => {
180
- if (a.score !== b.score) {
181
- return b.score - a.score;
182
- }
183
- if (a.recencyBoost !== b.recencyBoost) {
184
- return b.recencyBoost - a.recencyBoost;
185
- }
186
- return compareItemsByFuzzyScore(a.doc, b.doc, query, true, fileDocAccessor, scorerCache);
187
- });
188
-
189
- const topResults = scoredDocs.slice(0, maxResults);
190
- return {
191
- paths: topResults.map((item) => item.doc.path),
192
- isDirectory: topResults.map((item) => item.doc.isDirectory ?? false),
193
- };
194
- }
195
-
196
- function getBaselineResults(docs: ReadonlyArray<FileDoc>, maxResults: number): SearchResult {
197
- const withRecency = docs.map((doc) => ({
198
- doc,
199
- recency: getFileRecency(doc.path),
200
- }));
201
-
202
- const sorted = withRecency
203
- .sort((a, b) => {
204
- if (a.recency !== b.recency) {
205
- return b.recency - a.recency;
206
- }
207
- const aDepth = a.doc.path.split("/").length;
208
- const bDepth = b.doc.path.split("/").length;
209
- return aDepth - bDepth || a.doc.name.localeCompare(b.doc.name);
210
- })
211
- .slice(0, maxResults);
212
-
213
- return {
214
- paths: sorted.map((item) => item.doc.path),
215
- isDirectory: sorted.map((item) => item.doc.isDirectory ?? false),
216
- };
217
- }
218
-
219
- async function ensureIndex(
220
- options: Required<
221
- Pick<SearchOptions, "root" | "includeHidden" | "globs" | "ignoreGlobs" | "useRipgrep">
222
- > &
223
- Pick<SearchOptions, "useGitignore">,
224
- ): Promise<FileIndex> {
225
- const key = getIndexKey(options.root);
226
- const cached = fileIndexCache.get(key);
227
- if (cached) {
228
- if (cached.docs.length === 0 && !cached.building && !cached.buildError) {
229
- fileIndexCache.delete(key);
230
- } else {
231
- return cached;
232
- }
233
- }
234
-
235
- const index: FileIndex = { docs: [], building: true };
236
- fileIndexCache.set(key, index);
237
-
238
- const seenPaths = new Set<string>();
239
-
240
- const addDoc = (doc: FileDoc) => {
241
- if (seenPaths.has(doc.path)) {
242
- return;
243
- }
244
- seenPaths.add(doc.path);
245
- index.docs.push(doc);
246
- };
247
-
248
- if (options.useRipgrep !== false) {
249
- const args: string[] = ["--files", "--no-messages", "--max-depth", "100"];
250
-
251
- if (options.includeHidden) {
252
- args.push("--hidden");
253
- }
254
-
255
- if (options.useGitignore === false) {
256
- args.push("--no-ignore");
257
- }
258
-
259
- const include = options.globs ?? [];
260
- const ignore = options.ignoreGlobs ?? [];
261
-
262
- for (const g of include) {
263
- args.push("-g", g);
264
- }
265
- for (const g of ignore) {
266
- args.push("-g", g);
267
- }
268
-
269
- const child = spawn(rgPath, args, { cwd: options.root, stdio: ["ignore", "pipe", "pipe"] });
270
- const rl = createInterface({ input: child.stdout });
271
-
272
- rl.on("line", (line: string) => {
273
- const raw = unescapePath(line.trim());
274
- if (!raw) {
275
- return;
276
- }
277
- const absolute = path.resolve(options.root, raw);
278
- const doc = toDoc(absolute, false);
279
- if (!options.includeHidden) {
280
- if (
281
- doc.name.startsWith(".") ||
282
- doc.path.split("/").some((seg) => seg.startsWith(".") && seg !== ".")
283
- ) {
284
- return;
285
- }
286
- }
287
- addDoc(doc);
288
- });
289
-
290
- const performFallbackWalk = async () => {
291
- const MAX_DEPTH = 12;
292
- const MAX_FILES = 15000;
293
- const SKIP_DIRS = /^(node_modules|\.git|dist|build|out|coverage|\.vscode|\.idea|\.DS_Store)$/;
294
-
295
- const walk = async (dir: string, depth: number): Promise<void> => {
296
- if (depth > MAX_DEPTH || index.docs.length >= MAX_FILES) {
297
- return;
298
- }
299
-
300
- try {
301
- const entries = await fs.readdir(dir, { withFileTypes: true });
302
- const dirs: string[] = [];
303
-
304
- for (const entry of entries) {
305
- if (!options.includeHidden && entry.name.startsWith(".")) {
306
- continue;
307
- }
308
-
309
- const full = path.join(dir, entry.name);
310
-
311
- if (entry.isDirectory()) {
312
- if (SKIP_DIRS.test(entry.name)) {
313
- continue;
314
- }
315
- addDoc(toDoc(full, true));
316
- dirs.push(full);
317
- } else if (entry.isFile()) {
318
- addDoc(toDoc(full));
319
- if (index.docs.length >= MAX_FILES) {
320
- return;
321
- }
322
- }
323
- }
324
-
325
- await Promise.all(dirs.map((d) => walk(d, depth + 1)));
326
- } catch {
327
- // Skip directories we can't read
328
- }
329
- };
330
-
331
- await walk(options.root, 0);
332
- };
333
-
334
- const done = new Promise<void>((resolve) => {
335
- child.on("close", (code) => {
336
- if (code !== 0 && code !== null) {
337
- void performFallbackWalk()
338
- .then(() => {
339
- index.building = false;
340
- index.builtAt = Date.now();
341
- resolve();
342
- })
343
- .catch((err: unknown) => {
344
- index.building = false;
345
- index.buildError = err instanceof Error ? err : new Error("Indexing failed");
346
- resolve();
347
- });
348
- } else {
349
- // Collect directories after ripgrep finishes successfully
350
- void (async () => {
351
- const MAX_DEPTH = 12;
352
- const SKIP_DIRS =
353
- /^(node_modules|\.git|dist|build|out|coverage|\.vscode|\.idea|\.DS_Store)$/;
354
- const walk = async (dir: string, depth: number): Promise<void> => {
355
- if (depth > MAX_DEPTH) {
356
- return;
357
- }
358
- try {
359
- const entries = await fs.readdir(dir, { withFileTypes: true });
360
- await Promise.all(
361
- entries
362
- .filter(
363
- (e) =>
364
- e.isDirectory() &&
365
- (options.includeHidden || !e.name.startsWith(".")) &&
366
- !SKIP_DIRS.test(e.name),
367
- )
368
- .map(async (e) => {
369
- const full = path.join(dir, e.name);
370
- addDoc(toDoc(full, true));
371
- await walk(full, depth + 1);
372
- }),
373
- );
374
- } catch {}
375
- };
376
- await walk(options.root, 0);
377
- })().finally(() => {
378
- index.building = false;
379
- index.builtAt = Date.now();
380
- resolve();
381
- });
382
- }
383
- });
384
- child.on("error", () => {
385
- void performFallbackWalk()
386
- .then(() => {
387
- index.building = false;
388
- index.builtAt = Date.now();
389
- resolve();
390
- })
391
- .catch((err: unknown) => {
392
- index.building = false;
393
- index.buildError = err instanceof Error ? err : new Error("Indexing failed");
394
- resolve();
395
- });
396
- });
397
- });
398
-
399
- void done.then(() => {
400
- if (index.building) {
401
- index.building = false;
402
- index.builtAt = Date.now();
403
- }
404
- });
405
- } else {
406
- const MAX_DEPTH = 12;
407
- const MAX_FILES = 15000;
408
- const SKIP_DIRS = /^(node_modules|\.git|dist|build|out|coverage|\.vscode|\.idea|\.DS_Store)$/;
409
-
410
- const walk = async (dir: string, depth: number): Promise<void> => {
411
- if (depth > MAX_DEPTH || index.docs.length >= MAX_FILES) {
412
- return;
413
- }
414
-
415
- try {
416
- const entries = await fs.readdir(dir, { withFileTypes: true });
417
- const dirs: string[] = [];
418
-
419
- for (const entry of entries) {
420
- if (!options.includeHidden && entry.name.startsWith(".")) {
421
- continue;
422
- }
423
-
424
- const full = path.join(dir, entry.name);
425
-
426
- if (entry.isDirectory()) {
427
- if (SKIP_DIRS.test(entry.name)) {
428
- continue;
429
- }
430
- addDoc(toDoc(full, true));
431
- dirs.push(full);
432
- } else if (entry.isFile()) {
433
- addDoc(toDoc(full));
434
- if (index.docs.length >= MAX_FILES) {
435
- return;
436
- }
437
- }
438
- }
439
-
440
- await Promise.all(dirs.map((d) => walk(d, depth + 1)));
441
- } catch {}
442
- };
443
-
444
- void walk(options.root, 0)
445
- .then(() => {
446
- index.building = false;
447
- index.builtAt = Date.now();
448
- })
449
- .catch((err: unknown) => {
450
- index.building = false;
451
- index.buildError = err instanceof Error ? err : new Error("Indexing failed");
452
- });
453
- }
454
-
455
- return index;
456
- }
457
-
458
- export async function* searchFiles(
459
- query: string,
460
- options: SearchOptions,
461
- ): AsyncGenerator<SearchResult, void, void> {
462
- const root = options.root;
463
- const maxResults = options.maxResults ?? 100;
464
- const includeHidden = options.includeHidden ?? false;
465
- const globs = options.globs ?? [];
466
- const ignoreGlobs = options.ignoreGlobs ?? [];
467
- const useRipgrep = options.useRipgrep ?? true;
468
- const useGitignore = options.useGitignore ?? true;
469
-
470
- const stat = await fs.stat(root).catch(() => null);
471
- if (!stat || !stat.isDirectory()) {
472
- return;
473
- }
474
-
475
- const index = await ensureIndex({
476
- root,
477
- includeHidden,
478
- globs,
479
- ignoreGlobs,
480
- useRipgrep,
481
- useGitignore,
482
- });
483
-
484
- const trimmedQuery = query.trim();
485
- const cachedKey = getResultCacheKey(trimmedQuery, { root, maxResults });
486
- const cached = searchResultCache.get(cachedKey);
487
- if (cached && cached.paths.length > 0) {
488
- yield cached;
489
- if (!index.building) {
490
- return;
491
- }
492
- }
493
-
494
- if (trimmedQuery.length === 0) {
495
- const baseline = getBaselineResults(index.docs, maxResults);
496
- if (baseline.paths.length > 0) {
497
- searchResultCache.set(cachedKey, baseline);
498
- yield baseline;
499
- }
500
- return;
501
- }
502
-
503
- const preparedQuery = prepareQuery(trimmedQuery);
504
- const results = searchAndRank(index.docs, preparedQuery, maxResults);
505
-
506
- if (results.paths.length > 0) {
507
- searchResultCache.set(cachedKey, results);
508
- yield results;
509
- }
510
-
511
- if (index.building) {
512
- await new Promise<void>((resolve) => {
513
- const checkInterval = setInterval(() => {
514
- if (!index.building) {
515
- clearTimeout(timeout);
516
- clearInterval(checkInterval);
517
- resolve();
518
- }
519
- }, 50);
520
- const timeout = setTimeout(() => {
521
- clearInterval(checkInterval);
522
- resolve();
523
- }, 5000);
524
- });
525
-
526
- const finalResults = searchAndRank(index.docs, preparedQuery, maxResults);
527
- if (finalResults.paths.length > 0) {
528
- const finalKey = getResultCacheKey(trimmedQuery, { root, maxResults });
529
- searchResultCache.set(finalKey, finalResults);
530
- yield finalResults;
531
- }
532
- }
533
- }