@rama_nigg/open-cursor 2.3.19 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -48
- package/dist/cli/discover.js +177 -8
- package/dist/cli/mcptool.js +234 -5649
- package/dist/cli/opencode-cursor.js +930 -50
- package/dist/index.js +899 -5952
- package/dist/plugin-entry.js +877 -5932
- package/package.json +4 -2
- package/src/auth.ts +3 -1
- package/src/cli/model-discovery.ts +3 -2
- package/src/cli/opencode-cursor.ts +402 -23
- package/src/client/simple.ts +6 -3
- package/src/mcp/config.ts +49 -0
- package/src/mcp/tool-bridge.ts +1 -1
- package/src/models/discovery.ts +3 -2
- package/src/models/pricing.ts +196 -0
- package/src/models/variants.ts +446 -0
- package/src/plugin-toggle.ts +7 -1
- package/src/plugin.ts +167 -36
- package/src/provider/boundary.ts +10 -0
- package/src/provider/tool-loop-guard.ts +8 -3
- package/src/proxy/formatter.ts +30 -12
- package/src/proxy/prompt-builder.ts +10 -1
- package/src/streaming/types.ts +5 -0
- package/src/tools/defaults.ts +166 -0
- package/src/tools/executors/cli.ts +1 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +57 -0
package/src/tools/defaults.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolRegistry } from "./core/registry.js";
|
|
2
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Register default OpenCode tools in the registry
|
|
@@ -265,6 +266,10 @@ export function registerDefaultTools(registry: ToolRegistry): void {
|
|
|
265
266
|
const path = args.path as string;
|
|
266
267
|
const include = args.include as string | undefined;
|
|
267
268
|
|
|
269
|
+
if (process.platform === "win32") {
|
|
270
|
+
return nodeFallbackGrep(pattern, path, include);
|
|
271
|
+
}
|
|
272
|
+
|
|
268
273
|
const grepArgs = ["-r", "-n"];
|
|
269
274
|
if (include) {
|
|
270
275
|
grepArgs.push(`--include=${include}`);
|
|
@@ -374,6 +379,11 @@ export function registerDefaultTools(registry: ToolRegistry): void {
|
|
|
374
379
|
const path = resolvePathArg(args, "glob");
|
|
375
380
|
const cwd = path || ".";
|
|
376
381
|
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
382
|
+
|
|
383
|
+
if (process.platform === "win32") {
|
|
384
|
+
return nodeFallbackGlob(normalizedPattern, cwd);
|
|
385
|
+
}
|
|
386
|
+
|
|
377
387
|
const isPathPattern = normalizedPattern.includes("/");
|
|
378
388
|
const findArgs = [cwd, "-type", "f"];
|
|
379
389
|
if (isPathPattern) {
|
|
@@ -703,3 +713,159 @@ function coerceToString(value: unknown): string | null {
|
|
|
703
713
|
export function getDefaultToolNames(): string[] {
|
|
704
714
|
return ["bash", "read", "write", "edit", "grep", "ls", "glob", "mkdir", "rm", "stat"];
|
|
705
715
|
}
|
|
716
|
+
|
|
717
|
+
const FALLBACK_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build"]);
|
|
718
|
+
const fallbackLog = createLogger("tools:fallback");
|
|
719
|
+
|
|
720
|
+
export async function nodeFallbackGrep(
|
|
721
|
+
pattern: string,
|
|
722
|
+
searchPath: string,
|
|
723
|
+
include?: string,
|
|
724
|
+
): Promise<string> {
|
|
725
|
+
const fs = await import("fs/promises");
|
|
726
|
+
const path = await import("path");
|
|
727
|
+
|
|
728
|
+
let regex: RegExp;
|
|
729
|
+
try {
|
|
730
|
+
regex = new RegExp(pattern);
|
|
731
|
+
} catch {
|
|
732
|
+
return "Invalid regex pattern";
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
let includeRegex: RegExp | undefined;
|
|
736
|
+
if (include) {
|
|
737
|
+
const incPattern = include.replace(/\./g, "\\.").replace(/\?/g, ".").replace(/\*/g, ".*");
|
|
738
|
+
includeRegex = new RegExp(`^${incPattern}$`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const results: string[] = [];
|
|
742
|
+
|
|
743
|
+
async function walk(dir: string): Promise<void> {
|
|
744
|
+
if (results.length >= 100) return;
|
|
745
|
+
let entries;
|
|
746
|
+
try {
|
|
747
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
748
|
+
} catch (err: any) {
|
|
749
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
750
|
+
fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
for (const entry of entries) {
|
|
755
|
+
if (results.length >= 100) return;
|
|
756
|
+
const fullPath = path.join(dir, entry.name);
|
|
757
|
+
if (entry.isDirectory()) {
|
|
758
|
+
if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
759
|
+
await walk(fullPath);
|
|
760
|
+
}
|
|
761
|
+
} else if (entry.isFile()) {
|
|
762
|
+
if (includeRegex && !includeRegex.test(entry.name)) continue;
|
|
763
|
+
let content: string;
|
|
764
|
+
try {
|
|
765
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
766
|
+
} catch (err: any) {
|
|
767
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
768
|
+
fallbackLog.error("Unexpected error reading file", { path: fullPath, code: err?.code, message: err?.message });
|
|
769
|
+
}
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const lines = content.split("\n");
|
|
773
|
+
for (let i = 0; i < lines.length; i++) {
|
|
774
|
+
if (regex.test(lines[i])) {
|
|
775
|
+
results.push(`${fullPath}:${i + 1}:${lines[i]}`);
|
|
776
|
+
if (results.length >= 100) break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let stat;
|
|
784
|
+
try {
|
|
785
|
+
stat = await fs.stat(searchPath);
|
|
786
|
+
} catch {
|
|
787
|
+
return "Path not found";
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (stat.isFile()) {
|
|
791
|
+
let content: string;
|
|
792
|
+
try {
|
|
793
|
+
content = await fs.readFile(searchPath, "utf-8");
|
|
794
|
+
} catch (err: any) {
|
|
795
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
796
|
+
fallbackLog.error("Unexpected error reading file", { path: searchPath, code: err?.code, message: err?.message });
|
|
797
|
+
}
|
|
798
|
+
return "Path not found";
|
|
799
|
+
}
|
|
800
|
+
const lines = content.split("\n");
|
|
801
|
+
for (let i = 0; i < lines.length; i++) {
|
|
802
|
+
if (regex.test(lines[i])) {
|
|
803
|
+
results.push(`${searchPath}:${i + 1}:${lines[i]}`);
|
|
804
|
+
if (results.length >= 100) break;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
await walk(searchPath);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return results.join("\n") || "No matches found";
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export async function nodeFallbackGlob(
|
|
815
|
+
pattern: string,
|
|
816
|
+
searchPath: string,
|
|
817
|
+
): Promise<string> {
|
|
818
|
+
const fs = await import("fs/promises");
|
|
819
|
+
const path = await import("path");
|
|
820
|
+
|
|
821
|
+
const results: string[] = [];
|
|
822
|
+
const isPathPattern = pattern.includes("/");
|
|
823
|
+
|
|
824
|
+
// Handle ** before * so double-star → .* and single-star → [^/]*
|
|
825
|
+
let regexPattern = pattern
|
|
826
|
+
.replace(/\./g, "\\.")
|
|
827
|
+
.replace(/\*\*/g, "\x00") // placeholder for **
|
|
828
|
+
.replace(/\*/g, "[^/]*")
|
|
829
|
+
.replace(/\x00/g, ".*"); // restore ** as .*
|
|
830
|
+
|
|
831
|
+
let regex: RegExp;
|
|
832
|
+
try {
|
|
833
|
+
regex = isPathPattern
|
|
834
|
+
? new RegExp(`${regexPattern}$`)
|
|
835
|
+
: new RegExp(`^${regexPattern}$`);
|
|
836
|
+
} catch {
|
|
837
|
+
return "No files found";
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function walk(dir: string): Promise<void> {
|
|
841
|
+
if (results.length >= 50) return;
|
|
842
|
+
let entries;
|
|
843
|
+
try {
|
|
844
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
845
|
+
} catch (err: any) {
|
|
846
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
847
|
+
fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
for (const entry of entries) {
|
|
852
|
+
if (results.length >= 50) return;
|
|
853
|
+
const fullPath = path.join(dir, entry.name);
|
|
854
|
+
if (entry.isDirectory()) {
|
|
855
|
+
if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
856
|
+
await walk(fullPath);
|
|
857
|
+
}
|
|
858
|
+
} else if (entry.isFile()) {
|
|
859
|
+
const matchTarget = isPathPattern
|
|
860
|
+
? fullPath.replace(/\\/g, "/")
|
|
861
|
+
: entry.name;
|
|
862
|
+
if (regex.test(matchTarget)) {
|
|
863
|
+
results.push(fullPath);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
await walk(searchPath);
|
|
870
|
+
return results.join("\n") || "No files found";
|
|
871
|
+
}
|
|
@@ -16,6 +16,7 @@ export class CliExecutor implements IToolExecutor {
|
|
|
16
16
|
const { spawn } = await import("node:child_process");
|
|
17
17
|
const child = spawn("opencode", ["tool", "run", toolId, "--json", JSON.stringify(args)], {
|
|
18
18
|
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
shell: process.platform === "win32",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
const stdoutChunks: Buffer[] = [];
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { StreamJsonResultEvent } from "./streaming/types.js";
|
|
2
|
+
|
|
3
|
+
export type CursorUsageMetrics = {
|
|
4
|
+
inputTokens: number;
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
reasoningTokens: number;
|
|
7
|
+
cacheReadTokens: number;
|
|
8
|
+
cacheWriteTokens: number;
|
|
9
|
+
cost?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type OpenAiUsage = {
|
|
13
|
+
prompt_tokens: number;
|
|
14
|
+
completion_tokens: number;
|
|
15
|
+
total_tokens: number;
|
|
16
|
+
prompt_tokens_details: {
|
|
17
|
+
cached_tokens: number;
|
|
18
|
+
cache_write_tokens: number;
|
|
19
|
+
};
|
|
20
|
+
completion_tokens_details: {
|
|
21
|
+
reasoning_tokens: number;
|
|
22
|
+
};
|
|
23
|
+
cost?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function readTokenCount(value: unknown): number {
|
|
27
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
return Math.floor(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readOptionalCost(value: unknown): number | undefined {
|
|
34
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeCursorUsage(value: unknown): CursorUsageMetrics | undefined {
|
|
41
|
+
if (!value || typeof value !== "object") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const usage = value as Record<string, unknown>;
|
|
46
|
+
const metrics: CursorUsageMetrics = {
|
|
47
|
+
inputTokens: readTokenCount(usage.inputTokens ?? usage.input_tokens ?? usage.prompt_tokens),
|
|
48
|
+
outputTokens: readTokenCount(usage.outputTokens ?? usage.output_tokens ?? usage.completion_tokens),
|
|
49
|
+
reasoningTokens: readTokenCount(usage.reasoningTokens ?? usage.reasoning_tokens),
|
|
50
|
+
cacheReadTokens: readTokenCount(usage.cacheReadTokens ?? usage.cache_read_tokens),
|
|
51
|
+
cacheWriteTokens: readTokenCount(usage.cacheWriteTokens ?? usage.cache_write_tokens),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const cost = readOptionalCost(usage.cost ?? usage.totalCost ?? usage.total_cost);
|
|
55
|
+
if (cost !== undefined) {
|
|
56
|
+
metrics.cost = cost;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hasUsage =
|
|
60
|
+
metrics.inputTokens > 0
|
|
61
|
+
|| metrics.outputTokens > 0
|
|
62
|
+
|| metrics.reasoningTokens > 0
|
|
63
|
+
|| metrics.cacheReadTokens > 0
|
|
64
|
+
|| metrics.cacheWriteTokens > 0
|
|
65
|
+
|| cost !== undefined;
|
|
66
|
+
|
|
67
|
+
return hasUsage ? metrics : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createOpenAiUsage(metrics: CursorUsageMetrics): OpenAiUsage {
|
|
71
|
+
const promptTokens = metrics.inputTokens + metrics.cacheReadTokens + metrics.cacheWriteTokens;
|
|
72
|
+
const totalTokens = promptTokens + metrics.outputTokens + metrics.reasoningTokens;
|
|
73
|
+
const usage: OpenAiUsage = {
|
|
74
|
+
prompt_tokens: promptTokens,
|
|
75
|
+
completion_tokens: metrics.outputTokens,
|
|
76
|
+
total_tokens: totalTokens,
|
|
77
|
+
prompt_tokens_details: {
|
|
78
|
+
cached_tokens: metrics.cacheReadTokens,
|
|
79
|
+
cache_write_tokens: metrics.cacheWriteTokens,
|
|
80
|
+
},
|
|
81
|
+
completion_tokens_details: {
|
|
82
|
+
reasoning_tokens: metrics.reasoningTokens,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (metrics.cost !== undefined) {
|
|
87
|
+
usage.cost = metrics.cost;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return usage;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function extractOpenAiUsageFromResult(event: StreamJsonResultEvent): OpenAiUsage | undefined {
|
|
94
|
+
const metrics = normalizeCursorUsage(event.usage);
|
|
95
|
+
return metrics ? createOpenAiUsage(metrics) : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createChatCompletionUsageChunk(
|
|
99
|
+
id: string,
|
|
100
|
+
created: number,
|
|
101
|
+
model: string,
|
|
102
|
+
usage: OpenAiUsage,
|
|
103
|
+
) {
|
|
104
|
+
return {
|
|
105
|
+
id,
|
|
106
|
+
object: "chat.completion.chunk",
|
|
107
|
+
created,
|
|
108
|
+
model,
|
|
109
|
+
choices: [],
|
|
110
|
+
usage,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/utils/binary.ts
|
|
2
|
+
//
|
|
3
|
+
// Resolves the cursor-agent executable path. On Windows the binary is a `.cmd`
|
|
4
|
+
// shim, which Node's spawn cannot execute directly without `shell: true` —
|
|
5
|
+
// callers therefore pair this resolver with `shell: process.platform === "win32"`
|
|
6
|
+
// at every spawn site. That re-enables shell metacharacter interpretation, so
|
|
7
|
+
// any user-controlled string passed as an argument on Windows must be treated
|
|
8
|
+
// as untrusted; never concatenate user input into argv on win32.
|
|
9
|
+
import { existsSync as fsExistsSync } from "fs";
|
|
10
|
+
import * as pathModule from "path";
|
|
11
|
+
import { homedir as osHomedir } from "os";
|
|
12
|
+
import { createLogger } from "./logger.js";
|
|
13
|
+
|
|
14
|
+
const log = createLogger("binary");
|
|
15
|
+
|
|
16
|
+
export type BinaryDeps = {
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
env?: Record<string, string | undefined>;
|
|
19
|
+
existsSync?: (path: string) => boolean;
|
|
20
|
+
homedir?: () => string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function resolveCursorAgentBinary(deps: BinaryDeps = {}): string {
|
|
24
|
+
const platform = deps.platform ?? process.platform;
|
|
25
|
+
const env = deps.env ?? process.env;
|
|
26
|
+
const checkExists = deps.existsSync ?? fsExistsSync;
|
|
27
|
+
const home = (deps.homedir ?? osHomedir)();
|
|
28
|
+
|
|
29
|
+
const envOverride = env.CURSOR_AGENT_EXECUTABLE;
|
|
30
|
+
if (envOverride && envOverride.length > 0) {
|
|
31
|
+
return envOverride;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (platform === "win32") {
|
|
35
|
+
const pathJoin = pathModule.win32.join;
|
|
36
|
+
const localAppData = env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local");
|
|
37
|
+
const knownPath = pathJoin(localAppData, "cursor-agent", "cursor-agent.cmd");
|
|
38
|
+
if (checkExists(knownPath)) {
|
|
39
|
+
return knownPath;
|
|
40
|
+
}
|
|
41
|
+
log.warn("cursor-agent not found at known Windows path, falling back to PATH", { checkedPath: knownPath });
|
|
42
|
+
return "cursor-agent.cmd";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const knownPaths = [
|
|
46
|
+
pathModule.join(home, ".cursor-agent", "cursor-agent"),
|
|
47
|
+
"/usr/local/bin/cursor-agent",
|
|
48
|
+
];
|
|
49
|
+
for (const p of knownPaths) {
|
|
50
|
+
if (checkExists(p)) {
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
log.warn("cursor-agent not found at known paths, falling back to PATH", { checkedPaths: knownPaths });
|
|
56
|
+
return "cursor-agent";
|
|
57
|
+
}
|