@rama_nigg/open-cursor 2.3.20 → 2.4.1

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.
@@ -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
@@ -40,7 +41,7 @@ export function registerDefaultTools(registry: ToolRegistry): void {
40
41
 
41
42
  return new Promise<string>((resolve, reject) => {
42
43
  const proc = spawn(command, {
43
- shell: process.env.SHELL || "/bin/bash",
44
+ shell: resolveShellOption(),
44
45
  cwd,
45
46
  });
46
47
 
@@ -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) {
@@ -630,6 +640,20 @@ function resolveTimeoutMs(value: unknown): number {
630
640
  return raw <= 600 ? raw * 1000 : raw;
631
641
  }
632
642
 
643
+ export function resolveShellOption(deps: {
644
+ platform?: NodeJS.Platform;
645
+ env?: Record<string, string | undefined>;
646
+ } = {}): string | boolean {
647
+ const platform = deps.platform ?? process.platform;
648
+ const env = deps.env ?? process.env;
649
+
650
+ if (platform === "win32") {
651
+ return env.ComSpec || env.COMSPEC || true;
652
+ }
653
+
654
+ return env.SHELL || "/bin/bash";
655
+ }
656
+
633
657
  function resolveBoolean(value: unknown, defaultValue: boolean): boolean {
634
658
  if (typeof value === "boolean") {
635
659
  return value;
@@ -703,3 +727,159 @@ function coerceToString(value: unknown): string | null {
703
727
  export function getDefaultToolNames(): string[] {
704
728
  return ["bash", "read", "write", "edit", "grep", "ls", "glob", "mkdir", "rm", "stat"];
705
729
  }
730
+
731
+ const FALLBACK_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build"]);
732
+ const fallbackLog = createLogger("tools:fallback");
733
+
734
+ export async function nodeFallbackGrep(
735
+ pattern: string,
736
+ searchPath: string,
737
+ include?: string,
738
+ ): Promise<string> {
739
+ const fs = await import("fs/promises");
740
+ const path = await import("path");
741
+
742
+ let regex: RegExp;
743
+ try {
744
+ regex = new RegExp(pattern);
745
+ } catch {
746
+ return "Invalid regex pattern";
747
+ }
748
+
749
+ let includeRegex: RegExp | undefined;
750
+ if (include) {
751
+ const incPattern = include.replace(/\./g, "\\.").replace(/\?/g, ".").replace(/\*/g, ".*");
752
+ includeRegex = new RegExp(`^${incPattern}$`);
753
+ }
754
+
755
+ const results: string[] = [];
756
+
757
+ async function walk(dir: string): Promise<void> {
758
+ if (results.length >= 100) return;
759
+ let entries;
760
+ try {
761
+ entries = await fs.readdir(dir, { withFileTypes: true });
762
+ } catch (err: any) {
763
+ if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
764
+ fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
765
+ }
766
+ return;
767
+ }
768
+ for (const entry of entries) {
769
+ if (results.length >= 100) return;
770
+ const fullPath = path.join(dir, entry.name);
771
+ if (entry.isDirectory()) {
772
+ if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
773
+ await walk(fullPath);
774
+ }
775
+ } else if (entry.isFile()) {
776
+ if (includeRegex && !includeRegex.test(entry.name)) continue;
777
+ let content: string;
778
+ try {
779
+ content = await fs.readFile(fullPath, "utf-8");
780
+ } catch (err: any) {
781
+ if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
782
+ fallbackLog.error("Unexpected error reading file", { path: fullPath, code: err?.code, message: err?.message });
783
+ }
784
+ continue;
785
+ }
786
+ const lines = content.split("\n");
787
+ for (let i = 0; i < lines.length; i++) {
788
+ if (regex.test(lines[i])) {
789
+ results.push(`${fullPath}:${i + 1}:${lines[i]}`);
790
+ if (results.length >= 100) break;
791
+ }
792
+ }
793
+ }
794
+ }
795
+ }
796
+
797
+ let stat;
798
+ try {
799
+ stat = await fs.stat(searchPath);
800
+ } catch {
801
+ return "Path not found";
802
+ }
803
+
804
+ if (stat.isFile()) {
805
+ let content: string;
806
+ try {
807
+ content = await fs.readFile(searchPath, "utf-8");
808
+ } catch (err: any) {
809
+ if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
810
+ fallbackLog.error("Unexpected error reading file", { path: searchPath, code: err?.code, message: err?.message });
811
+ }
812
+ return "Path not found";
813
+ }
814
+ const lines = content.split("\n");
815
+ for (let i = 0; i < lines.length; i++) {
816
+ if (regex.test(lines[i])) {
817
+ results.push(`${searchPath}:${i + 1}:${lines[i]}`);
818
+ if (results.length >= 100) break;
819
+ }
820
+ }
821
+ } else {
822
+ await walk(searchPath);
823
+ }
824
+
825
+ return results.join("\n") || "No matches found";
826
+ }
827
+
828
+ export async function nodeFallbackGlob(
829
+ pattern: string,
830
+ searchPath: string,
831
+ ): Promise<string> {
832
+ const fs = await import("fs/promises");
833
+ const path = await import("path");
834
+
835
+ const results: string[] = [];
836
+ const isPathPattern = pattern.includes("/");
837
+
838
+ // Handle ** before * so double-star → .* and single-star → [^/]*
839
+ let regexPattern = pattern
840
+ .replace(/\./g, "\\.")
841
+ .replace(/\*\*/g, "\x00") // placeholder for **
842
+ .replace(/\*/g, "[^/]*")
843
+ .replace(/\x00/g, ".*"); // restore ** as .*
844
+
845
+ let regex: RegExp;
846
+ try {
847
+ regex = isPathPattern
848
+ ? new RegExp(`${regexPattern}$`)
849
+ : new RegExp(`^${regexPattern}$`);
850
+ } catch {
851
+ return "No files found";
852
+ }
853
+
854
+ async function walk(dir: string): Promise<void> {
855
+ if (results.length >= 50) return;
856
+ let entries;
857
+ try {
858
+ entries = await fs.readdir(dir, { withFileTypes: true });
859
+ } catch (err: any) {
860
+ if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
861
+ fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
862
+ }
863
+ return;
864
+ }
865
+ for (const entry of entries) {
866
+ if (results.length >= 50) return;
867
+ const fullPath = path.join(dir, entry.name);
868
+ if (entry.isDirectory()) {
869
+ if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
870
+ await walk(fullPath);
871
+ }
872
+ } else if (entry.isFile()) {
873
+ const matchTarget = isPathPattern
874
+ ? fullPath.replace(/\\/g, "/")
875
+ : entry.name;
876
+ if (regex.test(matchTarget)) {
877
+ results.push(fullPath);
878
+ }
879
+ }
880
+ }
881
+ }
882
+
883
+ await walk(searchPath);
884
+ return results.join("\n") || "No files found";
885
+ }
@@ -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
+ }