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

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 (198) hide show
  1. package/README.md +25 -0
  2. package/dist/index.mjs +466 -438
  3. package/package.json +4 -1
  4. package/.oxlintrc.json +0 -8
  5. package/resources/abacus.ico +0 -0
  6. package/resources/entitlements.plist +0 -9
  7. package/src/__e2e__/README.md +0 -196
  8. package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
  9. package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
  10. package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
  11. package/src/__e2e__/conversation.e2e.test.tsx +0 -56
  12. package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
  13. package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
  14. package/src/__e2e__/helpers/test-helpers.ts +0 -449
  15. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
  16. package/src/__e2e__/llm-models.e2e.test.ts +0 -402
  17. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
  18. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
  19. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
  20. package/src/__e2e__/repl.e2e.test.tsx +0 -78
  21. package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
  22. package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
  23. package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
  24. package/src/args.ts +0 -22
  25. package/src/components/__tests__/react-compiler.test.tsx +0 -78
  26. package/src/components/__tests__/status-indicator.test.tsx +0 -403
  27. package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
  28. package/src/components/composer/agent-mode-indicator.tsx +0 -63
  29. package/src/components/composer/bash-runner.tsx +0 -54
  30. package/src/components/composer/commands/default-commands.tsx +0 -615
  31. package/src/components/composer/commands/handler.tsx +0 -59
  32. package/src/components/composer/commands/picker.tsx +0 -273
  33. package/src/components/composer/commands/registry.ts +0 -233
  34. package/src/components/composer/commands/types.ts +0 -33
  35. package/src/components/composer/context.tsx +0 -88
  36. package/src/components/composer/file-mention-picker.tsx +0 -83
  37. package/src/components/composer/help.tsx +0 -44
  38. package/src/components/composer/index.tsx +0 -1007
  39. package/src/components/composer/mentions.ts +0 -57
  40. package/src/components/composer/message-queue.tsx +0 -70
  41. package/src/components/composer/mode-panel.tsx +0 -35
  42. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
  43. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
  44. package/src/components/composer/modes/bash-handler.tsx +0 -132
  45. package/src/components/composer/modes/bash-renderer.tsx +0 -175
  46. package/src/components/composer/modes/default-handlers.tsx +0 -33
  47. package/src/components/composer/modes/index.ts +0 -41
  48. package/src/components/composer/modes/types.ts +0 -21
  49. package/src/components/composer/persistent-shell.ts +0 -283
  50. package/src/components/composer/process.ts +0 -65
  51. package/src/components/composer/types.ts +0 -9
  52. package/src/components/composer/use-mention-search.ts +0 -68
  53. package/src/components/error-boundry.tsx +0 -60
  54. package/src/components/exit-message.tsx +0 -29
  55. package/src/components/expanded-view.tsx +0 -74
  56. package/src/components/file-completion.tsx +0 -127
  57. package/src/components/header.tsx +0 -47
  58. package/src/components/logo.tsx +0 -37
  59. package/src/components/segments.tsx +0 -356
  60. package/src/components/status-indicator.tsx +0 -306
  61. package/src/components/tool-group-summary.tsx +0 -263
  62. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -319
  63. package/src/components/tool-permissions/diff-preview.tsx +0 -359
  64. package/src/components/tool-permissions/index.ts +0 -5
  65. package/src/components/tool-permissions/permission-options.tsx +0 -401
  66. package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
  67. package/src/components/tool-permissions/tool-permission-ui.tsx +0 -420
  68. package/src/components/tools/agent/ask-user-question.tsx +0 -107
  69. package/src/components/tools/agent/enter-plan-mode.tsx +0 -55
  70. package/src/components/tools/agent/exit-plan-mode.tsx +0 -83
  71. package/src/components/tools/agent/handoff-to-main.tsx +0 -27
  72. package/src/components/tools/agent/subagent.tsx +0 -37
  73. package/src/components/tools/agent/todo-write.tsx +0 -104
  74. package/src/components/tools/browser/close-tab.tsx +0 -58
  75. package/src/components/tools/browser/computer.tsx +0 -70
  76. package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
  77. package/src/components/tools/browser/get-tab-content.tsx +0 -51
  78. package/src/components/tools/browser/navigate-to.tsx +0 -59
  79. package/src/components/tools/browser/new-tab.tsx +0 -60
  80. package/src/components/tools/browser/perform-action.tsx +0 -63
  81. package/src/components/tools/browser/refresh-tab.tsx +0 -43
  82. package/src/components/tools/browser/switch-tab.tsx +0 -58
  83. package/src/components/tools/filesystem/delete-file.tsx +0 -104
  84. package/src/components/tools/filesystem/edit.tsx +0 -220
  85. package/src/components/tools/filesystem/list-dir.tsx +0 -78
  86. package/src/components/tools/filesystem/read-file.tsx +0 -180
  87. package/src/components/tools/filesystem/upload-image.tsx +0 -76
  88. package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
  89. package/src/components/tools/index.ts +0 -91
  90. package/src/components/tools/mcp/mcp-tool.tsx +0 -158
  91. package/src/components/tools/search/fetch-url.tsx +0 -73
  92. package/src/components/tools/search/file-search.tsx +0 -78
  93. package/src/components/tools/search/grep.tsx +0 -90
  94. package/src/components/tools/search/semantic-search.tsx +0 -66
  95. package/src/components/tools/search/web-search.tsx +0 -71
  96. package/src/components/tools/shared/index.tsx +0 -48
  97. package/src/components/tools/shared/zod-coercion.ts +0 -35
  98. package/src/components/tools/terminal/bash-tool-output.tsx +0 -188
  99. package/src/components/tools/terminal/get-terminal-output.tsx +0 -91
  100. package/src/components/tools/terminal/run-in-terminal.tsx +0 -131
  101. package/src/components/tools/types.ts +0 -16
  102. package/src/components/tools.tsx +0 -68
  103. package/src/components/ui/__tests__/divider.test.tsx +0 -61
  104. package/src/components/ui/__tests__/gradient.test.tsx +0 -125
  105. package/src/components/ui/__tests__/input.test.tsx +0 -166
  106. package/src/components/ui/__tests__/select.test.tsx +0 -273
  107. package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
  108. package/src/components/ui/blinking-indicator.tsx +0 -27
  109. package/src/components/ui/divider.tsx +0 -162
  110. package/src/components/ui/gradient.tsx +0 -56
  111. package/src/components/ui/input.tsx +0 -228
  112. package/src/components/ui/select.tsx +0 -151
  113. package/src/components/ui/shimmer.tsx +0 -76
  114. package/src/context/agent-mode.tsx +0 -95
  115. package/src/context/extension-file.tsx +0 -136
  116. package/src/context/network-activity.tsx +0 -45
  117. package/src/context/notification.tsx +0 -62
  118. package/src/context/shell-size.tsx +0 -49
  119. package/src/context/shell-title.tsx +0 -38
  120. package/src/entrypoints/print-mode.ts +0 -312
  121. package/src/entrypoints/repl.tsx +0 -389
  122. package/src/hooks/use-agent.ts +0 -15
  123. package/src/hooks/use-api-client.ts +0 -1
  124. package/src/hooks/use-available-height.ts +0 -8
  125. package/src/hooks/use-cleanup.ts +0 -29
  126. package/src/hooks/use-interrupt-manager.ts +0 -242
  127. package/src/hooks/use-models.ts +0 -22
  128. package/src/index.ts +0 -217
  129. package/src/lib/__tests__/ansi.test.ts +0 -255
  130. package/src/lib/__tests__/cli.test.ts +0 -122
  131. package/src/lib/__tests__/commands.test.ts +0 -325
  132. package/src/lib/__tests__/constants.test.ts +0 -15
  133. package/src/lib/__tests__/focusables.test.ts +0 -25
  134. package/src/lib/__tests__/fs.test.ts +0 -231
  135. package/src/lib/__tests__/markdown.test.tsx +0 -348
  136. package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
  137. package/src/lib/__tests__/mcpManagement.test.ts +0 -38
  138. package/src/lib/__tests__/path-paste.test.ts +0 -144
  139. package/src/lib/__tests__/path.test.ts +0 -300
  140. package/src/lib/__tests__/queries.test.ts +0 -39
  141. package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
  142. package/src/lib/__tests__/text-buffer.test.ts +0 -328
  143. package/src/lib/__tests__/text-utils.test.ts +0 -32
  144. package/src/lib/__tests__/timing.test.ts +0 -78
  145. package/src/lib/__tests__/utils.test.ts +0 -238
  146. package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
  147. package/src/lib/ansi.ts +0 -150
  148. package/src/lib/cli-push-server.ts +0 -112
  149. package/src/lib/cli.ts +0 -44
  150. package/src/lib/clipboard.ts +0 -226
  151. package/src/lib/command-utils.ts +0 -93
  152. package/src/lib/commands.ts +0 -270
  153. package/src/lib/constants.ts +0 -3
  154. package/src/lib/extension-connection.ts +0 -181
  155. package/src/lib/focusables.ts +0 -7
  156. package/src/lib/fs.ts +0 -533
  157. package/src/lib/markdown/code-block.tsx +0 -63
  158. package/src/lib/markdown/index.ts +0 -4
  159. package/src/lib/markdown/link.tsx +0 -19
  160. package/src/lib/markdown/markdown.tsx +0 -372
  161. package/src/lib/markdown/types.ts +0 -15
  162. package/src/lib/mcpCommandHandler.ts +0 -121
  163. package/src/lib/mcpManagement.ts +0 -44
  164. package/src/lib/path-paste.ts +0 -185
  165. package/src/lib/path.ts +0 -179
  166. package/src/lib/queries.ts +0 -15
  167. package/src/lib/standaloneMcpService.ts +0 -688
  168. package/src/lib/status-utils.ts +0 -237
  169. package/src/lib/test-utils.tsx +0 -72
  170. package/src/lib/text-buffer.ts +0 -2415
  171. package/src/lib/text-utils.ts +0 -272
  172. package/src/lib/timing.ts +0 -63
  173. package/src/lib/types.ts +0 -295
  174. package/src/lib/utils.ts +0 -182
  175. package/src/lib/vim-buffer-actions.ts +0 -732
  176. package/src/providers/agent.tsx +0 -1063
  177. package/src/providers/api-client.tsx +0 -43
  178. package/src/services/logger.ts +0 -85
  179. package/src/terminal/detection.ts +0 -187
  180. package/src/terminal/exit.ts +0 -279
  181. package/src/terminal/notification.ts +0 -83
  182. package/src/terminal/progress.ts +0 -201
  183. package/src/terminal/setup.ts +0 -797
  184. package/src/terminal/types.ts +0 -51
  185. package/src/theme/context.tsx +0 -57
  186. package/src/theme/index.ts +0 -4
  187. package/src/theme/themed.tsx +0 -35
  188. package/src/theme/themes.json +0 -546
  189. package/src/theme/types.ts +0 -110
  190. package/src/tools/types.ts +0 -59
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +0 -33
  192. package/src/tools/utils/tool-ui-components.tsx +0 -649
  193. package/src/tools/utils/zod-coercion.ts +0 -35
  194. package/tsconfig.json +0 -16
  195. package/tsconfig.node.json +0 -29
  196. package/tsconfig.test.json +0 -27
  197. package/tsdown.config.ts +0 -17
  198. 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
- }