@easonwumac/computer-linker 0.1.2

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 (82) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/SECURITY.md +48 -0
  5. package/dist/api.d.ts +2 -0
  6. package/dist/api.js +360 -0
  7. package/dist/audit.d.ts +70 -0
  8. package/dist/audit.js +102 -0
  9. package/dist/capabilities.d.ts +98 -0
  10. package/dist/capabilities.js +718 -0
  11. package/dist/capability-policy.d.ts +22 -0
  12. package/dist/capability-policy.js +103 -0
  13. package/dist/chatgpt.d.ts +167 -0
  14. package/dist/chatgpt.js +561 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +4621 -0
  17. package/dist/client-smoke.d.ts +44 -0
  18. package/dist/client-smoke.js +639 -0
  19. package/dist/client.d.ts +217 -0
  20. package/dist/client.js +357 -0
  21. package/dist/codex-runs.d.ts +35 -0
  22. package/dist/codex-runs.js +66 -0
  23. package/dist/computer-contract.d.ts +33 -0
  24. package/dist/computer-contract.js +384 -0
  25. package/dist/computer-operation-registry.d.ts +45 -0
  26. package/dist/computer-operation-registry.js +179 -0
  27. package/dist/config-diagnostics.d.ts +11 -0
  28. package/dist/config-diagnostics.js +185 -0
  29. package/dist/config.d.ts +10 -0
  30. package/dist/config.js +69 -0
  31. package/dist/history-insights.d.ts +132 -0
  32. package/dist/history-insights.js +457 -0
  33. package/dist/http-auth.d.ts +3 -0
  34. package/dist/http-auth.js +15 -0
  35. package/dist/mcp-surface.d.ts +5 -0
  36. package/dist/mcp-surface.js +25 -0
  37. package/dist/oauth-provider.d.ts +52 -0
  38. package/dist/oauth-provider.js +325 -0
  39. package/dist/package-metadata.d.ts +7 -0
  40. package/dist/package-metadata.js +24 -0
  41. package/dist/permissions.d.ts +43 -0
  42. package/dist/permissions.js +150 -0
  43. package/dist/platform-shell.d.ts +28 -0
  44. package/dist/platform-shell.js +124 -0
  45. package/dist/processes.d.ts +50 -0
  46. package/dist/processes.js +178 -0
  47. package/dist/profile.d.ts +159 -0
  48. package/dist/profile.js +416 -0
  49. package/dist/screenshot.d.ts +47 -0
  50. package/dist/screenshot.js +302 -0
  51. package/dist/search.d.ts +34 -0
  52. package/dist/search.js +340 -0
  53. package/dist/security.d.ts +10 -0
  54. package/dist/security.js +108 -0
  55. package/dist/sensitive-files.d.ts +4 -0
  56. package/dist/sensitive-files.js +96 -0
  57. package/dist/server.d.ts +9 -0
  58. package/dist/server.js +713 -0
  59. package/dist/service.d.ts +125 -0
  60. package/dist/service.js +486 -0
  61. package/dist/sessions.d.ts +26 -0
  62. package/dist/sessions.js +34 -0
  63. package/dist/tunnels.d.ts +161 -0
  64. package/dist/tunnels.js +1243 -0
  65. package/dist/workspace-operations.d.ts +170 -0
  66. package/dist/workspace-operations.js +3219 -0
  67. package/dist/workspaces.d.ts +61 -0
  68. package/dist/workspaces.js +353 -0
  69. package/docs/agent-instructions.md +65 -0
  70. package/docs/alpha-evidence.example.json +54 -0
  71. package/docs/api-compatibility.md +56 -0
  72. package/docs/architecture.md +561 -0
  73. package/docs/chatgpt-setup.md +397 -0
  74. package/docs/client-recipes.md +98 -0
  75. package/docs/client-sdk.md +163 -0
  76. package/docs/computer-operation-v1.schema.json +143 -0
  77. package/docs/manual-test-plan.md +322 -0
  78. package/docs/product-spec.md +911 -0
  79. package/docs/release-checklist.md +285 -0
  80. package/docs/service-mode.md +99 -0
  81. package/examples/minimal-mcp-client.mjs +114 -0
  82. package/package.json +87 -0
@@ -0,0 +1,302 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir, readFile, rename, rm } from "node:fs/promises";
3
+ import { platform, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { promisify } from "node:util";
7
+ import { executableCommand, findExecutableCommand, windowsVerbatimArgumentsOption } from "./platform-shell.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const windowsScreenshotCommandEnv = "COMPUTER_LINKER_WINDOWS_SCREENSHOT_COMMAND";
10
+ const legacyWindowsScreenshotCommandEnv = "WORKSPACE_LINKER_WINDOWS_SCREENSHOT_COMMAND";
11
+ export function screenshotCapability() {
12
+ const provider = screenshotProvider();
13
+ if (!provider.available) {
14
+ return {
15
+ permission: provider.permission,
16
+ provider: provider.name,
17
+ supported: false,
18
+ modes: [],
19
+ displays: [],
20
+ windows: [],
21
+ };
22
+ }
23
+ return {
24
+ permission: provider.permission,
25
+ provider: provider.name,
26
+ supported: true,
27
+ modes: provider.modes,
28
+ displays: [{ id: "primary", primary: true }],
29
+ windows: [],
30
+ };
31
+ }
32
+ export function listScreenshotTargets() {
33
+ return screenshotCapability();
34
+ }
35
+ export async function captureScreenshot(options) {
36
+ const provider = screenshotProvider();
37
+ if (!provider.available) {
38
+ throw new Error(provider.permission.detail ?? "screenshot provider is unavailable on this platform");
39
+ }
40
+ if (options.format && options.format !== "png") {
41
+ throw new Error("only png screenshot format is currently supported");
42
+ }
43
+ validateScreenshotBounds(options);
44
+ if (options.source === "process") {
45
+ throw new Error("screen.capture_process is not implemented for this platform provider yet");
46
+ }
47
+ if (options.source === "display" && options.target && options.target !== "primary") {
48
+ throw new Error("only the primary display target is currently supported");
49
+ }
50
+ if (options.source === "window" && !options.target) {
51
+ throw new Error("window id is required for screen.capture_window");
52
+ }
53
+ if (!provider.modes.includes(options.source)) {
54
+ throw new Error(`screen.${options.source} capture is not implemented for ${provider.name}`);
55
+ }
56
+ const dir = join(tmpdir(), "computer-linker-screenshots");
57
+ await mkdir(dir, { recursive: true });
58
+ const file = join(dir, `screenshot-${randomUUID()}.png`);
59
+ const args = provider.captureArgs(options, file);
60
+ try {
61
+ const command = executableCommand(provider.command, args);
62
+ await execFileAsync(command.command, command.args, {
63
+ timeout: 30_000,
64
+ windowsHide: true,
65
+ ...windowsVerbatimArgumentsOption(command),
66
+ });
67
+ }
68
+ catch (error) {
69
+ await rm(file, { force: true });
70
+ throw new Error(`screenshot capture failed: ${error instanceof Error ? error.message : String(error)}`);
71
+ }
72
+ try {
73
+ await downscaleScreenshotIfNeeded(file, options);
74
+ }
75
+ catch (error) {
76
+ await rm(file, { force: true });
77
+ throw error;
78
+ }
79
+ const bytes = await readFile(file);
80
+ const dimensions = pngDimensions(bytes);
81
+ const returnMode = options.returnMode ?? "fileRef";
82
+ const result = {
83
+ format: "png",
84
+ ...dimensions,
85
+ sizeBytes: bytes.byteLength,
86
+ source: {
87
+ type: options.source,
88
+ id: options.target || "primary",
89
+ },
90
+ permission: provider.permission,
91
+ provider: provider.name,
92
+ };
93
+ if (returnMode === "base64" || returnMode === "bytes") {
94
+ result.bytesBase64 = bytes.toString("base64");
95
+ await rm(file, { force: true });
96
+ return result;
97
+ }
98
+ if (returnMode !== "fileRef") {
99
+ await rm(file, { force: true });
100
+ throw new Error("screenshot return must be one of: fileRef, base64, bytes");
101
+ }
102
+ result.fileRef = file;
103
+ return result;
104
+ }
105
+ function screenshotProvider() {
106
+ if (platform() === "darwin") {
107
+ const command = findExecutableCommand("screencapture") ?? "/usr/sbin/screencapture";
108
+ return {
109
+ name: "macos-screencapture",
110
+ available: true,
111
+ command,
112
+ modes: ["display", "window"],
113
+ permission: {
114
+ status: "unknown",
115
+ detail: "macOS may prompt for Screen Recording permission when capture is requested.",
116
+ },
117
+ captureArgs: (options, file) => options.source === "window"
118
+ ? ["-x", "-t", "png", "-l", String(options.target), file]
119
+ : ["-x", "-t", "png", file],
120
+ };
121
+ }
122
+ if (platform() === "win32") {
123
+ const command = process.env[windowsScreenshotCommandEnv]
124
+ ?? process.env[legacyWindowsScreenshotCommandEnv]
125
+ ?? findExecutableCommand("powershell")
126
+ ?? findExecutableCommand("powershell.exe")
127
+ ?? findExecutableCommand("pwsh");
128
+ if (!command) {
129
+ return {
130
+ name: "windows-powershell-screenshot",
131
+ available: false,
132
+ command: "",
133
+ modes: [],
134
+ permission: {
135
+ status: "unsupported",
136
+ detail: "Windows screenshot capture requires PowerShell or PowerShell Core on PATH.",
137
+ },
138
+ captureArgs: () => [],
139
+ };
140
+ }
141
+ return {
142
+ name: "windows-powershell-screenshot",
143
+ available: true,
144
+ command,
145
+ modes: ["display"],
146
+ permission: {
147
+ status: "unknown",
148
+ detail: "Windows desktop capture uses PowerShell and may fail in headless or non-interactive sessions.",
149
+ },
150
+ captureArgs: (_options, file) => [
151
+ "-NoLogo",
152
+ "-NoProfile",
153
+ "-NonInteractive",
154
+ "-ExecutionPolicy",
155
+ "Bypass",
156
+ "-Command",
157
+ powershellScriptBlock(windowsDisplayCaptureScript()),
158
+ file,
159
+ ],
160
+ };
161
+ }
162
+ return {
163
+ name: `${platform()}-screenshot`,
164
+ available: false,
165
+ command: "",
166
+ modes: [],
167
+ permission: {
168
+ status: "unsupported",
169
+ detail: `screenshot capture provider is not implemented for ${platform()} yet`,
170
+ },
171
+ captureArgs: () => [],
172
+ };
173
+ }
174
+ async function downscaleScreenshotIfNeeded(file, options) {
175
+ if (!options.maxWidth && !options.maxHeight)
176
+ return;
177
+ const dimensions = pngDimensions(await readFile(file));
178
+ if (!dimensions.width || !dimensions.height) {
179
+ throw new Error("unable to determine PNG dimensions for screenshot downscaling");
180
+ }
181
+ const target = downscaledDimensions(dimensions.width, dimensions.height, options);
182
+ if (target.width === dimensions.width && target.height === dimensions.height) {
183
+ return;
184
+ }
185
+ const tempFile = join(tmpdir(), "computer-linker-screenshots", `screenshot-resized-${randomUUID()}.png`);
186
+ try {
187
+ if (platform() === "win32") {
188
+ await downscalePngWithPowerShell(file, tempFile, target);
189
+ }
190
+ else if (platform() === "darwin") {
191
+ await downscalePngWithSips(file, tempFile, target);
192
+ }
193
+ else {
194
+ throw new Error(`screenshot downscaling is not supported on ${platform()} yet`);
195
+ }
196
+ await rename(tempFile, file);
197
+ }
198
+ catch (error) {
199
+ await rm(tempFile, { force: true });
200
+ throw new Error(`screenshot downscaling failed: ${error instanceof Error ? error.message : String(error)}`);
201
+ }
202
+ }
203
+ function downscaledDimensions(width, height, options) {
204
+ const widthScale = options.maxWidth && width > options.maxWidth ? options.maxWidth / width : 1;
205
+ const heightScale = options.maxHeight && height > options.maxHeight ? options.maxHeight / height : 1;
206
+ const scale = Math.min(widthScale, heightScale);
207
+ return {
208
+ width: Math.max(1, Math.round(width * scale)),
209
+ height: Math.max(1, Math.round(height * scale)),
210
+ };
211
+ }
212
+ async function downscalePngWithPowerShell(file, tempFile, target) {
213
+ const command = findExecutableCommand("powershell")
214
+ ?? findExecutableCommand("powershell.exe")
215
+ ?? findExecutableCommand("pwsh");
216
+ if (!command) {
217
+ throw new Error("PowerShell is required for Windows screenshot downscaling");
218
+ }
219
+ const args = [
220
+ "-NoLogo",
221
+ "-NoProfile",
222
+ "-NonInteractive",
223
+ "-ExecutionPolicy",
224
+ "Bypass",
225
+ "-Command",
226
+ powershellScriptBlock(windowsDownscaleScript()),
227
+ file,
228
+ tempFile,
229
+ String(target.width),
230
+ String(target.height),
231
+ ];
232
+ const executable = executableCommand(command, args);
233
+ await execFileAsync(executable.command, executable.args, {
234
+ timeout: 30_000,
235
+ windowsHide: true,
236
+ ...windowsVerbatimArgumentsOption(executable),
237
+ });
238
+ }
239
+ async function downscalePngWithSips(file, tempFile, target) {
240
+ const command = findExecutableCommand("sips") ?? "/usr/bin/sips";
241
+ await execFileAsync(command, ["-s", "format", "png", "-z", String(target.height), String(target.width), file, "--out", tempFile], {
242
+ timeout: 30_000,
243
+ });
244
+ }
245
+ function validateScreenshotBounds(options) {
246
+ for (const [label, value] of [["maxWidth", options.maxWidth], ["maxHeight", options.maxHeight]]) {
247
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
248
+ throw new Error(`screenshot ${label} must be a positive integer`);
249
+ }
250
+ }
251
+ }
252
+ function windowsDisplayCaptureScript() {
253
+ return [
254
+ "param([string]$Path)",
255
+ "Add-Type -AssemblyName System.Windows.Forms",
256
+ "Add-Type -AssemblyName System.Drawing",
257
+ "$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds",
258
+ "$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height",
259
+ "$graphics = [System.Drawing.Graphics]::FromImage($bitmap)",
260
+ "try {",
261
+ " $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)",
262
+ " $bitmap.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)",
263
+ "} finally {",
264
+ " $graphics.Dispose()",
265
+ " $bitmap.Dispose()",
266
+ "}",
267
+ ].join("; ");
268
+ }
269
+ function powershellScriptBlock(script) {
270
+ return `& { ${script} }`;
271
+ }
272
+ function windowsDownscaleScript() {
273
+ return [
274
+ "param([string]$Path, [string]$Output, [int]$Width, [int]$Height)",
275
+ "Add-Type -AssemblyName System.Drawing",
276
+ "$image = [System.Drawing.Image]::FromFile($Path)",
277
+ "$bitmap = $null",
278
+ "$graphics = $null",
279
+ "try {",
280
+ " $bitmap = New-Object System.Drawing.Bitmap $Width, $Height",
281
+ " $graphics = [System.Drawing.Graphics]::FromImage($bitmap)",
282
+ " $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic",
283
+ " $graphics.DrawImage($image, 0, 0, $Width, $Height)",
284
+ " $bitmap.Save($Output, [System.Drawing.Imaging.ImageFormat]::Png)",
285
+ "} finally {",
286
+ " if ($graphics -ne $null) { $graphics.Dispose() }",
287
+ " if ($bitmap -ne $null) { $bitmap.Dispose() }",
288
+ " $image.Dispose()",
289
+ "}",
290
+ ].join("; ");
291
+ }
292
+ function pngDimensions(bytes) {
293
+ if (bytes.byteLength >= 24 &&
294
+ bytes.toString("ascii", 1, 4) === "PNG" &&
295
+ bytes.toString("ascii", 12, 16) === "IHDR") {
296
+ return {
297
+ width: bytes.readUInt32BE(16),
298
+ height: bytes.readUInt32BE(20),
299
+ };
300
+ }
301
+ return {};
302
+ }
@@ -0,0 +1,34 @@
1
+ export interface SearchTextOptions {
2
+ cwd: string;
3
+ query: string;
4
+ glob?: string;
5
+ fixedStrings: boolean;
6
+ caseSensitive: boolean;
7
+ maxResults: number;
8
+ beforeContext?: number;
9
+ afterContext?: number;
10
+ }
11
+ export interface FindFilesOptions {
12
+ cwd: string;
13
+ pattern: string;
14
+ maxResults: number;
15
+ }
16
+ export interface SearchSymbolsOptions {
17
+ cwd: string;
18
+ query?: string;
19
+ glob?: string;
20
+ caseSensitive: boolean;
21
+ maxResults: number;
22
+ maxBytes: number;
23
+ }
24
+ export interface SymbolMatch {
25
+ path: string;
26
+ line: number;
27
+ column: number;
28
+ name: string;
29
+ kind: string;
30
+ signature: string;
31
+ }
32
+ export declare function searchText(options: SearchTextOptions): Promise<string>;
33
+ export declare function searchSymbols(options: SearchSymbolsOptions): Promise<SymbolMatch[]>;
34
+ export declare function findFiles(options: FindFilesOptions): Promise<string>;
package/dist/search.js ADDED
@@ -0,0 +1,340 @@
1
+ import { execFile } from "node:child_process";
2
+ import { opendir, readFile } from "node:fs/promises";
3
+ import { join, relative } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { isSensitiveWorkspacePath, sensitiveFileRgGlobArgs } from "./sensitive-files.js";
6
+ const execFileAsync = promisify(execFile);
7
+ export async function searchText(options) {
8
+ const args = [
9
+ "--line-number",
10
+ "--color",
11
+ "never",
12
+ "--path-separator",
13
+ "/",
14
+ "--hidden",
15
+ "--glob",
16
+ "!{.git,node_modules,dist,build,.next,.cache}/**",
17
+ ];
18
+ if (options.fixedStrings)
19
+ args.push("--fixed-strings");
20
+ if (!options.caseSensitive)
21
+ args.push("--ignore-case");
22
+ if (options.beforeContext && options.beforeContext > 0)
23
+ args.push("--before-context", String(options.beforeContext));
24
+ if (options.afterContext && options.afterContext > 0)
25
+ args.push("--after-context", String(options.afterContext));
26
+ if (options.glob)
27
+ args.push("--glob", options.glob);
28
+ args.push(...sensitiveFileRgGlobArgs());
29
+ args.push(options.query, ".");
30
+ try {
31
+ const { stdout } = await execFileAsync("rg", args, {
32
+ cwd: options.cwd,
33
+ maxBuffer: 1024 * 1024 * 10,
34
+ });
35
+ return normalizeRipgrepMatchPaths(limitLines(stdout, options.maxResults)) || "No matches.";
36
+ }
37
+ catch (error) {
38
+ if (isExecError(error) && error.code === 1)
39
+ return "No matches.";
40
+ if (isExecError(error) && error.code === "ENOENT") {
41
+ return fallbackSearchText(options);
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ export async function searchSymbols(options) {
47
+ const files = await candidateSymbolFiles(options);
48
+ const matches = [];
49
+ const query = options.query
50
+ ? options.caseSensitive ? options.query : options.query.toLowerCase()
51
+ : undefined;
52
+ for (const file of files) {
53
+ if (matches.length >= options.maxResults)
54
+ break;
55
+ let content;
56
+ try {
57
+ content = await readFile(join(options.cwd, file), "utf8");
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (content.includes("\0"))
63
+ continue;
64
+ if (Buffer.byteLength(content, "utf8") > options.maxBytes) {
65
+ content = content.slice(0, options.maxBytes);
66
+ }
67
+ const lines = content.split(/\r?\n/);
68
+ for (let index = 0; index < lines.length; index++) {
69
+ for (const symbol of symbolsFromLine(lines[index])) {
70
+ const haystack = options.caseSensitive
71
+ ? `${symbol.name}\n${symbol.signature}`
72
+ : `${symbol.name}\n${symbol.signature}`.toLowerCase();
73
+ if (query && !haystack.includes(query))
74
+ continue;
75
+ matches.push({
76
+ path: file,
77
+ line: index + 1,
78
+ column: symbol.column,
79
+ name: symbol.name,
80
+ kind: symbol.kind,
81
+ signature: symbol.signature,
82
+ });
83
+ if (matches.length >= options.maxResults)
84
+ return matches;
85
+ }
86
+ }
87
+ }
88
+ return matches;
89
+ }
90
+ export async function findFiles(options) {
91
+ try {
92
+ const { stdout } = await execFileAsync("rg", [
93
+ "--files",
94
+ "--path-separator",
95
+ "/",
96
+ "--hidden",
97
+ "--glob",
98
+ "!{.git,node_modules,dist,build,.next,.cache}/**",
99
+ "--glob",
100
+ options.pattern,
101
+ ...sensitiveFileRgGlobArgs(),
102
+ ], {
103
+ cwd: options.cwd,
104
+ maxBuffer: 1024 * 1024 * 10,
105
+ });
106
+ return normalizePathLines(limitLines(stdout, options.maxResults)) || "No files found.";
107
+ }
108
+ catch (error) {
109
+ if (isExecError(error) && error.code === 1)
110
+ return "No files found.";
111
+ if (isExecError(error) && error.code === "ENOENT") {
112
+ const files = await fallbackFindFiles(options.cwd, options.pattern, options.maxResults);
113
+ return files.join("\n") || "No files found.";
114
+ }
115
+ throw error;
116
+ }
117
+ }
118
+ async function candidateSymbolFiles(options) {
119
+ const args = [
120
+ "--files",
121
+ "--path-separator",
122
+ "/",
123
+ "--hidden",
124
+ "--glob",
125
+ "!{.git,node_modules,dist,build,.next,.cache}/**",
126
+ ];
127
+ for (const glob of SYMBOL_FILE_GLOBS)
128
+ args.push("--glob", glob);
129
+ if (options.glob)
130
+ args.push("--glob", options.glob);
131
+ args.push(...sensitiveFileRgGlobArgs());
132
+ try {
133
+ const { stdout } = await execFileAsync("rg", args, {
134
+ cwd: options.cwd,
135
+ maxBuffer: 1024 * 1024 * 10,
136
+ });
137
+ return stdout.trimEnd().split("\n").filter(Boolean).map(toPortablePath);
138
+ }
139
+ catch (error) {
140
+ if (isExecError(error) && error.code === 1)
141
+ return [];
142
+ if (isExecError(error) && error.code === "ENOENT") {
143
+ const files = await fallbackFindFiles(options.cwd, options.glob ?? "**/*", options.maxResults * 20);
144
+ return files.filter((file) => SYMBOL_EXTENSIONS.has(fileExtension(file)));
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ function symbolsFromLine(line) {
150
+ const trimmed = line.trim();
151
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
152
+ return [];
153
+ const symbols = [];
154
+ for (const rule of SYMBOL_RULES) {
155
+ const match = rule.pattern.exec(line);
156
+ if (!match?.groups?.name)
157
+ continue;
158
+ if (SYMBOL_KEYWORDS.has(match.groups.name))
159
+ continue;
160
+ symbols.push({
161
+ name: match.groups.name,
162
+ kind: rule.kind,
163
+ signature: trimmed,
164
+ column: line.indexOf(match.groups.name) + 1,
165
+ });
166
+ }
167
+ return symbols;
168
+ }
169
+ async function fallbackSearchText(options) {
170
+ const files = await fallbackFindFiles(options.cwd, "**/*", options.maxResults * 10);
171
+ const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
172
+ const matches = [];
173
+ for (const file of files) {
174
+ let content;
175
+ try {
176
+ content = await readFile(join(options.cwd, file), "utf8");
177
+ }
178
+ catch {
179
+ continue;
180
+ }
181
+ const lines = content.split("\n");
182
+ for (let index = 0; index < lines.length; index++) {
183
+ const haystack = options.caseSensitive ? lines[index] : lines[index].toLowerCase();
184
+ if (haystack.includes(needle)) {
185
+ for (let contextIndex = Math.max(0, index - (options.beforeContext ?? 0)); contextIndex <= Math.min(lines.length - 1, index + (options.afterContext ?? 0)); contextIndex++) {
186
+ const separator = contextIndex === index ? ":" : "-";
187
+ matches.push(`${file}${separator}${contextIndex + 1}${separator}${lines[contextIndex]}`);
188
+ }
189
+ if (matches.length >= options.maxResults)
190
+ return matches.join("\n");
191
+ }
192
+ }
193
+ }
194
+ return matches.join("\n") || "No matches.";
195
+ }
196
+ function fileExtension(path) {
197
+ const index = path.lastIndexOf(".");
198
+ return index === -1 ? "" : path.slice(index);
199
+ }
200
+ async function fallbackFindFiles(root, pattern, maxResults) {
201
+ const results = [];
202
+ const needle = globNeedle(pattern);
203
+ async function walk(directory) {
204
+ if (results.length >= maxResults)
205
+ return;
206
+ let entries;
207
+ try {
208
+ entries = await opendir(directory);
209
+ }
210
+ catch {
211
+ return;
212
+ }
213
+ for await (const entry of entries) {
214
+ if (SKIPPED_DIRECTORIES.has(entry.name))
215
+ continue;
216
+ const path = join(directory, entry.name);
217
+ if (entry.isDirectory()) {
218
+ await walk(path);
219
+ continue;
220
+ }
221
+ const relativePath = relative(root, path);
222
+ if (isSensitiveWorkspacePath(relativePath))
223
+ continue;
224
+ if (!needle || relativePath.includes(needle)) {
225
+ results.push(toPortablePath(relativePath));
226
+ if (results.length >= maxResults)
227
+ return;
228
+ }
229
+ }
230
+ }
231
+ await walk(root);
232
+ return results;
233
+ }
234
+ function globNeedle(pattern) {
235
+ return pattern.replace(/\*\*\/|\*|\{|\}/g, "").replace(/^\/+|\/+$/g, "");
236
+ }
237
+ function limitLines(text, maxLines) {
238
+ const lines = text.trimEnd().split("\n");
239
+ return lines.slice(0, maxLines).join("\n");
240
+ }
241
+ function normalizePathLines(text) {
242
+ return text
243
+ .split("\n")
244
+ .map(toPortablePath)
245
+ .join("\n");
246
+ }
247
+ function normalizeRipgrepMatchPaths(text) {
248
+ return text
249
+ .split("\n")
250
+ .map((line) => {
251
+ if (line === "--")
252
+ return line;
253
+ const match = /^(.*?)([:-]\d+[:-].*)$/.exec(line);
254
+ if (!match)
255
+ return line;
256
+ return `${toPortablePath(match[1])}${match[2]}`;
257
+ })
258
+ .join("\n");
259
+ }
260
+ function toPortablePath(path) {
261
+ return path.replace(/\\/g, "/");
262
+ }
263
+ function isExecError(error) {
264
+ return error instanceof Error && "code" in error;
265
+ }
266
+ const SKIPPED_DIRECTORIES = new Set([
267
+ ".git",
268
+ "node_modules",
269
+ "dist",
270
+ "build",
271
+ ".next",
272
+ ".cache",
273
+ ]);
274
+ const SYMBOL_FILE_GLOBS = [
275
+ "*.{ts,tsx,js,jsx,mjs,cjs}",
276
+ "*.{py,rb,go,rs,java,kt,kts,swift}",
277
+ "*.{c,h,cc,cpp,cxx,hpp,cs,php}",
278
+ ];
279
+ const SYMBOL_EXTENSIONS = new Set([
280
+ ".ts",
281
+ ".tsx",
282
+ ".js",
283
+ ".jsx",
284
+ ".mjs",
285
+ ".cjs",
286
+ ".py",
287
+ ".rb",
288
+ ".go",
289
+ ".rs",
290
+ ".java",
291
+ ".kt",
292
+ ".kts",
293
+ ".swift",
294
+ ".c",
295
+ ".h",
296
+ ".cc",
297
+ ".cpp",
298
+ ".cxx",
299
+ ".hpp",
300
+ ".cs",
301
+ ".php",
302
+ ]);
303
+ const IDENTIFIER = "[A-Za-z_$][\\w$]*";
304
+ const SYMBOL_RULES = [
305
+ { kind: "function", pattern: new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s+(?<name>${IDENTIFIER})\\s*\\(`) },
306
+ { kind: "function", pattern: new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var)\\s+(?<name>${IDENTIFIER})\\s*=\\s*(?:async\\s*)?(?:\\([^)]*\\)|${IDENTIFIER})\\s*=>`) },
307
+ { kind: "class", pattern: new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?class\\s+(?<name>${IDENTIFIER})\\b`) },
308
+ { kind: "interface", pattern: new RegExp(`^\\s*(?:export\\s+)?interface\\s+(?<name>${IDENTIFIER})\\b`) },
309
+ { kind: "type", pattern: new RegExp(`^\\s*(?:export\\s+)?type\\s+(?<name>${IDENTIFIER})\\b[^=]*=`) },
310
+ { kind: "enum", pattern: new RegExp(`^\\s*(?:export\\s+)?enum\\s+(?<name>${IDENTIFIER})\\b`) },
311
+ { kind: "function", pattern: new RegExp(`^\\s*(?:async\\s+)?def\\s+(?<name>${IDENTIFIER})\\s*\\(`) },
312
+ { kind: "class", pattern: new RegExp(`^\\s*class\\s+(?<name>${IDENTIFIER})\\b`) },
313
+ { kind: "function", pattern: new RegExp(`^\\s*def\\s+(?<name>[A-Za-z_]\\w*[!?=]?)\\b`) },
314
+ { kind: "module", pattern: new RegExp(`^\\s*module\\s+(?<name>${IDENTIFIER})\\b`) },
315
+ { kind: "function", pattern: new RegExp(`^\\s*func\\s+(?:\\([^)]*\\)\\s*)?(?<name>${IDENTIFIER})\\s*\\(`) },
316
+ { kind: "type", pattern: new RegExp(`^\\s*type\\s+(?<name>${IDENTIFIER})\\s+(?:struct|interface)\\b`) },
317
+ { kind: "function", pattern: new RegExp(`^\\s*(?:pub\\s+)?(?:async\\s+)?fn\\s+(?<name>${IDENTIFIER})\\s*\\(`) },
318
+ { kind: "class", pattern: new RegExp(`^\\s*(?:pub\\s+)?(?:struct|trait)\\s+(?<name>${IDENTIFIER})\\b`) },
319
+ { kind: "enum", pattern: new RegExp(`^\\s*(?:pub\\s+)?enum\\s+(?<name>${IDENTIFIER})\\b`) },
320
+ { kind: "class", pattern: new RegExp(`^\\s*(?:public\\s+|private\\s+|protected\\s+|internal\\s+|final\\s+|open\\s+|data\\s+|sealed\\s+|abstract\\s+)*class\\s+(?<name>${IDENTIFIER})\\b`) },
321
+ { kind: "interface", pattern: new RegExp(`^\\s*(?:public\\s+|private\\s+|protected\\s+|internal\\s+)*interface\\s+(?<name>${IDENTIFIER})\\b`) },
322
+ { kind: "enum", pattern: new RegExp(`^\\s*(?:public\\s+|private\\s+|protected\\s+|internal\\s+)*enum\\s+(?:class\\s+)?(?<name>${IDENTIFIER})\\b`) },
323
+ { kind: "function", pattern: new RegExp(`^\\s*(?:public\\s+|private\\s+|protected\\s+|internal\\s+|static\\s+|final\\s+|override\\s+|suspend\\s+)*fun\\s+(?<name>${IDENTIFIER})\\s*\\(`) },
324
+ { kind: "function", pattern: new RegExp(`^\\s*(?:public\\s+|private\\s+|protected\\s+|static\\s+|final\\s+|override\\s+|async\\s+)*[^=;{}()]+\\s+(?<name>${IDENTIFIER})\\s*\\([^;]*\\)\\s*(?:\\{|=>)`) },
325
+ { kind: "function", pattern: new RegExp(`^\\s*func\\s+(?<name>${IDENTIFIER})\\s*\\([^)]*\\)\\s*(?:async\\s+)?(?:throws\\s+)?(?:->\\s*[^{}]+\\s*)?\\{`) },
326
+ { kind: "class", pattern: new RegExp(`^\\s*(?:public\\s+|private\\s+|final\\s+|open\\s+)?(?:struct|protocol|actor)\\s+(?<name>${IDENTIFIER})\\b`) },
327
+ ];
328
+ const SYMBOL_KEYWORDS = new Set([
329
+ "async",
330
+ "await",
331
+ "catch",
332
+ "do",
333
+ "else",
334
+ "for",
335
+ "if",
336
+ "return",
337
+ "switch",
338
+ "try",
339
+ "while",
340
+ ]);
@@ -0,0 +1,10 @@
1
+ import type { LocalPortConfig } from "./permissions.js";
2
+ export type SecuritySeverity = "info" | "warning" | "critical";
3
+ export interface SecurityFinding {
4
+ id: string;
5
+ severity: SecuritySeverity;
6
+ title: string;
7
+ detail: string;
8
+ workspaceId?: string;
9
+ }
10
+ export declare function securityDiagnostics(config?: LocalPortConfig): SecurityFinding[];