@rama_nigg/open-cursor 2.2.0 → 2.3.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.
Files changed (58) hide show
  1. package/dist/cli/opencode-cursor.js +27 -8
  2. package/dist/index.js +44 -15
  3. package/dist/plugin-entry.js +44 -15
  4. package/package.json +9 -3
  5. package/src/acp/metrics.ts +83 -0
  6. package/src/acp/sessions.ts +107 -0
  7. package/src/acp/tools.ts +209 -0
  8. package/src/auth.ts +269 -0
  9. package/src/cli/discover.ts +53 -0
  10. package/src/cli/model-discovery.ts +50 -0
  11. package/src/cli/opencode-cursor.ts +620 -0
  12. package/src/client/simple.ts +277 -0
  13. package/src/commands/status.ts +39 -0
  14. package/src/index.ts +40 -0
  15. package/src/models/config.ts +64 -0
  16. package/src/models/discovery.ts +132 -0
  17. package/src/models/index.ts +3 -0
  18. package/src/models/types.ts +11 -0
  19. package/src/plugin-entry.ts +28 -0
  20. package/src/plugin-toggle.ts +67 -0
  21. package/src/plugin.ts +1918 -0
  22. package/src/provider/boundary.ts +161 -0
  23. package/src/provider/runtime-interception.ts +721 -0
  24. package/src/provider/tool-loop-guard.ts +644 -0
  25. package/src/provider/tool-schema-compat.ts +516 -0
  26. package/src/provider.ts +268 -0
  27. package/src/proxy/formatter.ts +42 -0
  28. package/src/proxy/handler.ts +29 -0
  29. package/src/proxy/prompt-builder.ts +171 -0
  30. package/src/proxy/server.ts +207 -0
  31. package/src/proxy/tool-loop.ts +317 -0
  32. package/src/proxy/types.ts +13 -0
  33. package/src/streaming/ai-sdk-parts.ts +105 -0
  34. package/src/streaming/delta-tracker.ts +33 -0
  35. package/src/streaming/line-buffer.ts +44 -0
  36. package/src/streaming/openai-sse.ts +114 -0
  37. package/src/streaming/parser.ts +22 -0
  38. package/src/streaming/types.ts +152 -0
  39. package/src/tools/core/executor.ts +25 -0
  40. package/src/tools/core/registry.ts +27 -0
  41. package/src/tools/core/types.ts +31 -0
  42. package/src/tools/defaults.ts +673 -0
  43. package/src/tools/discovery.ts +140 -0
  44. package/src/tools/executors/cli.ts +58 -0
  45. package/src/tools/executors/local.ts +25 -0
  46. package/src/tools/executors/mcp.ts +39 -0
  47. package/src/tools/executors/sdk.ts +39 -0
  48. package/src/tools/index.ts +8 -0
  49. package/src/tools/registry.ts +34 -0
  50. package/src/tools/router.ts +123 -0
  51. package/src/tools/schema.ts +58 -0
  52. package/src/tools/skills/loader.ts +61 -0
  53. package/src/tools/skills/resolver.ts +21 -0
  54. package/src/tools/types.ts +29 -0
  55. package/src/types.ts +8 -0
  56. package/src/utils/errors.ts +131 -0
  57. package/src/utils/logger.ts +146 -0
  58. package/src/utils/perf.ts +44 -0
@@ -0,0 +1,44 @@
1
+ export class LineBuffer {
2
+ private buffer = "";
3
+ private decoder = new TextDecoder();
4
+
5
+ push(chunk: string | Uint8Array): string[] {
6
+ const text = typeof chunk === "string" ? chunk : this.decoder.decode(chunk);
7
+ if (!text) {
8
+ return [];
9
+ }
10
+
11
+ this.buffer += text;
12
+ const lines = this.buffer.split("\n");
13
+ this.buffer = lines.pop() ?? "";
14
+
15
+ const completed: string[] = [];
16
+ for (const line of lines) {
17
+ const normalized = line.endsWith("\r") ? line.slice(0, -1) : line;
18
+ if (!normalized.trim()) {
19
+ continue;
20
+ }
21
+ completed.push(normalized);
22
+ }
23
+
24
+ return completed;
25
+ }
26
+
27
+ flush(): string[] {
28
+ if (!this.buffer.trim()) {
29
+ this.buffer = "";
30
+ return [];
31
+ }
32
+
33
+ const normalized = this.buffer.endsWith("\r")
34
+ ? this.buffer.slice(0, -1)
35
+ : this.buffer;
36
+ this.buffer = "";
37
+
38
+ if (!normalized.trim()) {
39
+ return [];
40
+ }
41
+
42
+ return [normalized];
43
+ }
44
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ extractText,
3
+ extractThinking,
4
+ inferToolName,
5
+ isAssistantText,
6
+ isThinking,
7
+ isToolCall,
8
+ type StreamJsonEvent,
9
+ type StreamJsonToolCallEvent,
10
+ } from "./types.js";
11
+ import { DeltaTracker } from "./delta-tracker.js";
12
+
13
+ type OpenAiToolCall = {
14
+ index: number;
15
+ id: string;
16
+ type: "function";
17
+ function: {
18
+ name: string;
19
+ arguments: string;
20
+ };
21
+ };
22
+
23
+ type OpenAiDelta = {
24
+ content?: string;
25
+ reasoning_content?: string;
26
+ tool_calls?: OpenAiToolCall[];
27
+ };
28
+
29
+ type OpenAiChunk = {
30
+ id: string;
31
+ object: "chat.completion.chunk";
32
+ created: number;
33
+ model: string;
34
+ choices: Array<{
35
+ index: number;
36
+ delta: OpenAiDelta;
37
+ finish_reason: string | null;
38
+ }>;
39
+ };
40
+
41
+ const createChunk = (id: string, created: number, model: string, delta: OpenAiDelta): OpenAiChunk => ({
42
+ id,
43
+ object: "chat.completion.chunk",
44
+ created,
45
+ model,
46
+ choices: [
47
+ {
48
+ index: 0,
49
+ delta,
50
+ finish_reason: null,
51
+ },
52
+ ],
53
+ });
54
+
55
+ export const formatSseChunk = (payload: object) => `data: ${JSON.stringify(payload)}\n\n`;
56
+
57
+ export const formatSseDone = () => "data: [DONE]\n\n";
58
+
59
+ export class StreamToSseConverter {
60
+ private readonly id: string;
61
+ private readonly created: number;
62
+ private readonly model: string;
63
+ private readonly tracker = new DeltaTracker();
64
+
65
+ constructor(model: string, options?: { id?: string; created?: number }) {
66
+ this.model = model;
67
+ this.id = options?.id ?? `cursor-acp-${Date.now()}`;
68
+ this.created = options?.created ?? Math.floor(Date.now() / 1000);
69
+ }
70
+
71
+ handleEvent(event: StreamJsonEvent): string[] {
72
+ if (isAssistantText(event)) {
73
+ const delta = this.tracker.nextText(extractText(event));
74
+ return delta ? [this.chunkWith({ content: delta })] : [];
75
+ }
76
+
77
+ if (isThinking(event)) {
78
+ const delta = this.tracker.nextThinking(extractThinking(event));
79
+ return delta ? [this.chunkWith({ reasoning_content: delta })] : [];
80
+ }
81
+
82
+ if (isToolCall(event)) {
83
+ return [this.chunkWith(this.toolCallDelta(event))];
84
+ }
85
+
86
+ return [];
87
+ }
88
+
89
+ private chunkWith(delta: OpenAiDelta): string {
90
+ return formatSseChunk(createChunk(this.id, this.created, this.model, delta));
91
+ }
92
+
93
+ private toolCallDelta(event: StreamJsonToolCallEvent): OpenAiDelta {
94
+ const id = event.call_id ?? "unknown";
95
+ const toolName = inferToolName(event) || "tool";
96
+ const toolKey = Object.keys(event.tool_call ?? {})[0];
97
+ const args = toolKey ? event.tool_call[toolKey]?.args : undefined;
98
+ const argumentsText = args ? JSON.stringify(args) : "";
99
+
100
+ return {
101
+ tool_calls: [
102
+ {
103
+ index: 0,
104
+ id,
105
+ type: "function",
106
+ function: {
107
+ name: toolName,
108
+ arguments: argumentsText,
109
+ },
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ }
@@ -0,0 +1,22 @@
1
+ import type { StreamJsonEvent } from "./types.js";
2
+ import { createLogger } from "../utils/logger.js";
3
+
4
+ const log = createLogger("streaming:parser");
5
+
6
+ export const parseStreamJsonLine = (line: string): StreamJsonEvent | null => {
7
+ const trimmed = line.trim();
8
+ if (!trimmed) {
9
+ return null;
10
+ }
11
+
12
+ try {
13
+ const parsed = JSON.parse(trimmed) as unknown;
14
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15
+ return null;
16
+ }
17
+ return parsed as StreamJsonEvent;
18
+ } catch {
19
+ log.debug("Failed to parse NDJSON line", { line: trimmed.substring(0, 100) });
20
+ return null;
21
+ }
22
+ };
@@ -0,0 +1,152 @@
1
+ export type StreamJsonTextContent = {
2
+ type: "text";
3
+ text: string;
4
+ };
5
+
6
+ export type StreamJsonThinkingContent = {
7
+ type: "thinking";
8
+ thinking: string;
9
+ };
10
+
11
+ export type StreamJsonContent = StreamJsonTextContent | StreamJsonThinkingContent;
12
+
13
+ export type StreamJsonSystemEvent = {
14
+ type: "system";
15
+ subtype?: string;
16
+ timestamp?: number;
17
+ session_id?: string;
18
+ cwd?: string;
19
+ model?: string;
20
+ permissionMode?: string;
21
+ tools?: Array<{ name: string; description?: string }>;
22
+ message?: string;
23
+ };
24
+
25
+ export type StreamJsonUserEvent = {
26
+ type: "user";
27
+ timestamp?: number;
28
+ session_id?: string;
29
+ message: {
30
+ role: "user";
31
+ content: StreamJsonContent[];
32
+ };
33
+ };
34
+
35
+ export type StreamJsonAssistantEvent = {
36
+ type: "assistant";
37
+ timestamp?: number;
38
+ session_id?: string;
39
+ message: {
40
+ role: "assistant";
41
+ content: StreamJsonContent[];
42
+ };
43
+ };
44
+
45
+ /**
46
+ * Thinking event from cursor-agent stream-json output.
47
+ * Real format: {"type":"thinking","subtype":"delta","text":"...","session_id":"...","timestamp_ms":...}
48
+ * OR: {"type":"thinking","subtype":"completed","session_id":"...","timestamp_ms":...}
49
+ */
50
+ export type StreamJsonThinkingEvent = {
51
+ type: "thinking";
52
+ subtype?: "delta" | "completed" | string;
53
+ text?: string;
54
+ timestamp?: number;
55
+ timestamp_ms?: number;
56
+ session_id?: string;
57
+ };
58
+
59
+ export type StreamJsonToolCallPayload = {
60
+ args?: Record<string, unknown>;
61
+ result?: Record<string, unknown>;
62
+ };
63
+
64
+ export type StreamJsonToolCallEvent = {
65
+ type: "tool_call";
66
+ subtype?: string;
67
+ timestamp?: number;
68
+ session_id?: string;
69
+ call_id?: string;
70
+ tool_call: Record<string, StreamJsonToolCallPayload>;
71
+ };
72
+
73
+ export type StreamJsonResultEvent = {
74
+ type: "result";
75
+ subtype?: "success" | "error" | string;
76
+ timestamp?: number;
77
+ session_id?: string;
78
+ is_error?: boolean;
79
+ error?: {
80
+ message?: string;
81
+ code?: number | string;
82
+ details?: string;
83
+ };
84
+ };
85
+
86
+ export type StreamJsonEvent =
87
+ | StreamJsonSystemEvent
88
+ | StreamJsonUserEvent
89
+ | StreamJsonAssistantEvent
90
+ | StreamJsonThinkingEvent
91
+ | StreamJsonToolCallEvent
92
+ | StreamJsonResultEvent;
93
+
94
+ const hasTextContent = (event: StreamJsonAssistantEvent) =>
95
+ event.message.content.some((content) => content.type === "text");
96
+
97
+ const hasThinkingContent = (event: StreamJsonAssistantEvent) =>
98
+ event.message.content.some((content) => content.type === "thinking");
99
+
100
+ export const isAssistantText = (event: StreamJsonEvent): event is StreamJsonAssistantEvent =>
101
+ event.type === "assistant" && hasTextContent(event);
102
+
103
+ export const isThinking = (
104
+ event: StreamJsonEvent,
105
+ ): event is StreamJsonAssistantEvent | StreamJsonThinkingEvent => {
106
+ if (event.type === "thinking") {
107
+ return true;
108
+ }
109
+
110
+ return event.type === "assistant" && hasThinkingContent(event);
111
+ };
112
+
113
+ export const isToolCall = (event: StreamJsonEvent): event is StreamJsonToolCallEvent =>
114
+ event.type === "tool_call";
115
+
116
+ export const isResult = (event: StreamJsonEvent): event is StreamJsonResultEvent =>
117
+ event.type === "result";
118
+
119
+ export const extractText = (event: StreamJsonAssistantEvent) =>
120
+ event.message.content
121
+ .filter((content): content is StreamJsonTextContent => content.type === "text")
122
+ .map((content) => content.text)
123
+ .join("");
124
+
125
+ export const extractThinking = (
126
+ event: StreamJsonAssistantEvent | StreamJsonThinkingEvent,
127
+ ): string => {
128
+ if (event.type === "thinking") {
129
+ return event.text ?? "";
130
+ }
131
+
132
+ return event.message.content
133
+ .filter(
134
+ (content): content is StreamJsonThinkingContent => content.type === "thinking",
135
+ )
136
+ .map((content) => content.thinking)
137
+ .join("");
138
+ };
139
+
140
+ export const inferToolName = (event: StreamJsonToolCallEvent) => {
141
+ const [key] = Object.keys(event.tool_call ?? {});
142
+ if (!key) {
143
+ return "";
144
+ }
145
+
146
+ if (key.endsWith("ToolCall")) {
147
+ const base = key.slice(0, -"ToolCall".length);
148
+ return base.charAt(0).toLowerCase() + base.slice(1);
149
+ }
150
+
151
+ return key;
152
+ };
@@ -0,0 +1,25 @@
1
+ import type { IToolExecutor, ExecutionResult } from "./types.js";
2
+ import { createLogger } from "../../utils/logger.js";
3
+
4
+ const log = createLogger("tools:executor:chain");
5
+
6
+ /**
7
+ * Executes using the first executor that declares it can handle the toolId.
8
+ */
9
+ export async function executeWithChain(
10
+ executors: IToolExecutor[],
11
+ toolId: string,
12
+ args: Record<string, unknown>
13
+ ): Promise<ExecutionResult> {
14
+ for (const ex of executors) {
15
+ if (ex.canExecute(toolId)) {
16
+ try {
17
+ return await ex.execute(toolId, args);
18
+ } catch (err: any) {
19
+ log.warn("Executor threw unexpected error", { toolId, error: String(err?.message || err) });
20
+ return { status: "error", error: String(err?.message || err) };
21
+ }
22
+ }
23
+ }
24
+ return { status: "error", error: `No executor available for ${toolId}` };
25
+ }
@@ -0,0 +1,27 @@
1
+ import type { ToolHandler, Tool } from "./types.js";
2
+
3
+ interface RegisteredTool {
4
+ tool: Tool;
5
+ handler: ToolHandler;
6
+ }
7
+
8
+ export class ToolRegistry {
9
+ private tools = new Map<string, RegisteredTool>();
10
+
11
+ register(tool: Tool, handler: ToolHandler): void {
12
+ this.tools.set(tool.name, { tool, handler });
13
+ }
14
+
15
+ getHandler(name: string): ToolHandler | undefined {
16
+ return this.tools.get(name)?.handler;
17
+ }
18
+
19
+ getTool(name: string): Tool | undefined {
20
+ return this.tools.get(name)?.tool;
21
+ }
22
+
23
+ list(): Tool[] {
24
+ return Array.from(this.tools.values()).map((t) => t.tool);
25
+ }
26
+ }
27
+
@@ -0,0 +1,31 @@
1
+ export interface ExecutionResult {
2
+ status: "success" | "error";
3
+ output?: string;
4
+ error?: string;
5
+ errorType?: "recoverable" | "fatal";
6
+ }
7
+
8
+ export interface IToolExecutor {
9
+ canExecute(toolId: string): boolean;
10
+ execute(toolId: string, args: Record<string, unknown>): Promise<ExecutionResult>;
11
+ }
12
+
13
+ export interface ToolHandler {
14
+ (args: Record<string, unknown>): Promise<string>;
15
+ }
16
+
17
+ export interface Tool {
18
+ id: string;
19
+ name: string;
20
+ description: string;
21
+ parameters: any;
22
+ source: "sdk" | "cli" | "local" | "mcp";
23
+ }
24
+
25
+ export interface Skill extends Tool {
26
+ aliases?: string[];
27
+ category?: string;
28
+ triggers?: string[];
29
+ prerequisites?: string[];
30
+ }
31
+