@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.
- package/dist/index.mjs +450 -422
- package/package.json +4 -1
- package/.oxlintrc.json +0 -8
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +0 -9
- package/src/__e2e__/README.md +0 -196
- package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
- package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
- package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
- package/src/__e2e__/conversation.e2e.test.tsx +0 -56
- package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
- package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
- package/src/__e2e__/helpers/test-helpers.ts +0 -449
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
- package/src/__e2e__/llm-models.e2e.test.ts +0 -402
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
- package/src/__e2e__/repl.e2e.test.tsx +0 -78
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
- package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
- package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
- package/src/args.ts +0 -22
- package/src/components/__tests__/react-compiler.test.tsx +0 -78
- package/src/components/__tests__/status-indicator.test.tsx +0 -403
- package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
- package/src/components/composer/agent-mode-indicator.tsx +0 -63
- package/src/components/composer/bash-runner.tsx +0 -54
- package/src/components/composer/commands/default-commands.tsx +0 -615
- package/src/components/composer/commands/handler.tsx +0 -59
- package/src/components/composer/commands/picker.tsx +0 -273
- package/src/components/composer/commands/registry.ts +0 -233
- package/src/components/composer/commands/types.ts +0 -33
- package/src/components/composer/context.tsx +0 -88
- package/src/components/composer/file-mention-picker.tsx +0 -83
- package/src/components/composer/help.tsx +0 -44
- package/src/components/composer/index.tsx +0 -1007
- package/src/components/composer/mentions.ts +0 -57
- package/src/components/composer/message-queue.tsx +0 -70
- package/src/components/composer/mode-panel.tsx +0 -35
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
- package/src/components/composer/modes/bash-handler.tsx +0 -132
- package/src/components/composer/modes/bash-renderer.tsx +0 -175
- package/src/components/composer/modes/default-handlers.tsx +0 -33
- package/src/components/composer/modes/index.ts +0 -41
- package/src/components/composer/modes/types.ts +0 -21
- package/src/components/composer/persistent-shell.ts +0 -283
- package/src/components/composer/process.ts +0 -65
- package/src/components/composer/types.ts +0 -9
- package/src/components/composer/use-mention-search.ts +0 -68
- package/src/components/error-boundry.tsx +0 -60
- package/src/components/exit-message.tsx +0 -29
- package/src/components/expanded-view.tsx +0 -74
- package/src/components/file-completion.tsx +0 -127
- package/src/components/header.tsx +0 -47
- package/src/components/logo.tsx +0 -37
- package/src/components/segments.tsx +0 -356
- package/src/components/status-indicator.tsx +0 -306
- package/src/components/tool-group-summary.tsx +0 -263
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -319
- package/src/components/tool-permissions/diff-preview.tsx +0 -359
- package/src/components/tool-permissions/index.ts +0 -5
- package/src/components/tool-permissions/permission-options.tsx +0 -401
- package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
- package/src/components/tool-permissions/tool-permission-ui.tsx +0 -420
- package/src/components/tools/agent/ask-user-question.tsx +0 -107
- package/src/components/tools/agent/enter-plan-mode.tsx +0 -55
- package/src/components/tools/agent/exit-plan-mode.tsx +0 -83
- package/src/components/tools/agent/handoff-to-main.tsx +0 -27
- package/src/components/tools/agent/subagent.tsx +0 -37
- package/src/components/tools/agent/todo-write.tsx +0 -104
- package/src/components/tools/browser/close-tab.tsx +0 -58
- package/src/components/tools/browser/computer.tsx +0 -70
- package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
- package/src/components/tools/browser/get-tab-content.tsx +0 -51
- package/src/components/tools/browser/navigate-to.tsx +0 -59
- package/src/components/tools/browser/new-tab.tsx +0 -60
- package/src/components/tools/browser/perform-action.tsx +0 -63
- package/src/components/tools/browser/refresh-tab.tsx +0 -43
- package/src/components/tools/browser/switch-tab.tsx +0 -58
- package/src/components/tools/filesystem/delete-file.tsx +0 -104
- package/src/components/tools/filesystem/edit.tsx +0 -220
- package/src/components/tools/filesystem/list-dir.tsx +0 -78
- package/src/components/tools/filesystem/read-file.tsx +0 -180
- package/src/components/tools/filesystem/upload-image.tsx +0 -76
- package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
- package/src/components/tools/index.ts +0 -91
- package/src/components/tools/mcp/mcp-tool.tsx +0 -158
- package/src/components/tools/search/fetch-url.tsx +0 -73
- package/src/components/tools/search/file-search.tsx +0 -78
- package/src/components/tools/search/grep.tsx +0 -90
- package/src/components/tools/search/semantic-search.tsx +0 -66
- package/src/components/tools/search/web-search.tsx +0 -71
- package/src/components/tools/shared/index.tsx +0 -48
- package/src/components/tools/shared/zod-coercion.ts +0 -35
- package/src/components/tools/terminal/bash-tool-output.tsx +0 -188
- package/src/components/tools/terminal/get-terminal-output.tsx +0 -91
- package/src/components/tools/terminal/run-in-terminal.tsx +0 -131
- package/src/components/tools/types.ts +0 -16
- package/src/components/tools.tsx +0 -68
- package/src/components/ui/__tests__/divider.test.tsx +0 -61
- package/src/components/ui/__tests__/gradient.test.tsx +0 -125
- package/src/components/ui/__tests__/input.test.tsx +0 -166
- package/src/components/ui/__tests__/select.test.tsx +0 -273
- package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
- package/src/components/ui/blinking-indicator.tsx +0 -27
- package/src/components/ui/divider.tsx +0 -162
- package/src/components/ui/gradient.tsx +0 -56
- package/src/components/ui/input.tsx +0 -228
- package/src/components/ui/select.tsx +0 -151
- package/src/components/ui/shimmer.tsx +0 -76
- package/src/context/agent-mode.tsx +0 -95
- package/src/context/extension-file.tsx +0 -136
- package/src/context/network-activity.tsx +0 -45
- package/src/context/notification.tsx +0 -62
- package/src/context/shell-size.tsx +0 -49
- package/src/context/shell-title.tsx +0 -38
- package/src/entrypoints/print-mode.ts +0 -312
- package/src/entrypoints/repl.tsx +0 -389
- package/src/hooks/use-agent.ts +0 -15
- package/src/hooks/use-api-client.ts +0 -1
- package/src/hooks/use-available-height.ts +0 -8
- package/src/hooks/use-cleanup.ts +0 -29
- package/src/hooks/use-interrupt-manager.ts +0 -242
- package/src/hooks/use-models.ts +0 -22
- package/src/index.ts +0 -217
- package/src/lib/__tests__/ansi.test.ts +0 -255
- package/src/lib/__tests__/cli.test.ts +0 -122
- package/src/lib/__tests__/commands.test.ts +0 -325
- package/src/lib/__tests__/constants.test.ts +0 -15
- package/src/lib/__tests__/focusables.test.ts +0 -25
- package/src/lib/__tests__/fs.test.ts +0 -231
- package/src/lib/__tests__/markdown.test.tsx +0 -348
- package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
- package/src/lib/__tests__/mcpManagement.test.ts +0 -38
- package/src/lib/__tests__/path-paste.test.ts +0 -144
- package/src/lib/__tests__/path.test.ts +0 -300
- package/src/lib/__tests__/queries.test.ts +0 -39
- package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
- package/src/lib/__tests__/text-buffer.test.ts +0 -328
- package/src/lib/__tests__/text-utils.test.ts +0 -32
- package/src/lib/__tests__/timing.test.ts +0 -78
- package/src/lib/__tests__/utils.test.ts +0 -238
- package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
- package/src/lib/ansi.ts +0 -150
- package/src/lib/cli-push-server.ts +0 -112
- package/src/lib/cli.ts +0 -44
- package/src/lib/clipboard.ts +0 -226
- package/src/lib/command-utils.ts +0 -93
- package/src/lib/commands.ts +0 -270
- package/src/lib/constants.ts +0 -3
- package/src/lib/extension-connection.ts +0 -181
- package/src/lib/focusables.ts +0 -7
- package/src/lib/fs.ts +0 -533
- package/src/lib/markdown/code-block.tsx +0 -63
- package/src/lib/markdown/index.ts +0 -4
- package/src/lib/markdown/link.tsx +0 -19
- package/src/lib/markdown/markdown.tsx +0 -372
- package/src/lib/markdown/types.ts +0 -15
- package/src/lib/mcpCommandHandler.ts +0 -121
- package/src/lib/mcpManagement.ts +0 -44
- package/src/lib/path-paste.ts +0 -185
- package/src/lib/path.ts +0 -179
- package/src/lib/queries.ts +0 -15
- package/src/lib/standaloneMcpService.ts +0 -688
- package/src/lib/status-utils.ts +0 -237
- package/src/lib/test-utils.tsx +0 -72
- package/src/lib/text-buffer.ts +0 -2415
- package/src/lib/text-utils.ts +0 -272
- package/src/lib/timing.ts +0 -63
- package/src/lib/types.ts +0 -295
- package/src/lib/utils.ts +0 -182
- package/src/lib/vim-buffer-actions.ts +0 -732
- package/src/providers/agent.tsx +0 -1063
- package/src/providers/api-client.tsx +0 -43
- package/src/services/logger.ts +0 -85
- package/src/terminal/detection.ts +0 -187
- package/src/terminal/exit.ts +0 -279
- package/src/terminal/notification.ts +0 -83
- package/src/terminal/progress.ts +0 -201
- package/src/terminal/setup.ts +0 -797
- package/src/terminal/types.ts +0 -51
- package/src/theme/context.tsx +0 -57
- package/src/theme/index.ts +0 -4
- package/src/theme/themed.tsx +0 -35
- package/src/theme/themes.json +0 -546
- package/src/theme/types.ts +0 -110
- package/src/tools/types.ts +0 -59
- package/src/tools/utils/__tests__/zod-coercion.test.ts +0 -33
- package/src/tools/utils/tool-ui-components.tsx +0 -649
- package/src/tools/utils/zod-coercion.ts +0 -35
- package/tsconfig.json +0 -16
- package/tsconfig.node.json +0 -29
- package/tsconfig.test.json +0 -27
- package/tsdown.config.ts +0 -17
- 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
|
-
}
|
package/src/lib/focusables.ts
DELETED
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
|
-
}
|