@rama_nigg/open-cursor 2.2.1 → 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.
- package/package.json +9 -3
- package/src/acp/metrics.ts +83 -0
- package/src/acp/sessions.ts +107 -0
- package/src/acp/tools.ts +209 -0
- package/src/auth.ts +269 -0
- package/src/cli/discover.ts +53 -0
- package/src/cli/model-discovery.ts +50 -0
- package/src/cli/opencode-cursor.ts +620 -0
- package/src/client/simple.ts +277 -0
- package/src/commands/status.ts +39 -0
- package/src/index.ts +40 -0
- package/src/models/config.ts +64 -0
- package/src/models/discovery.ts +132 -0
- package/src/models/index.ts +3 -0
- package/src/models/types.ts +11 -0
- package/src/plugin-entry.ts +28 -0
- package/src/plugin-toggle.ts +67 -0
- package/src/plugin.ts +1918 -0
- package/src/provider/boundary.ts +161 -0
- package/src/provider/runtime-interception.ts +721 -0
- package/src/provider/tool-loop-guard.ts +644 -0
- package/src/provider/tool-schema-compat.ts +516 -0
- package/src/provider.ts +268 -0
- package/src/proxy/formatter.ts +42 -0
- package/src/proxy/handler.ts +29 -0
- package/src/proxy/prompt-builder.ts +171 -0
- package/src/proxy/server.ts +207 -0
- package/src/proxy/tool-loop.ts +317 -0
- package/src/proxy/types.ts +13 -0
- package/src/streaming/ai-sdk-parts.ts +105 -0
- package/src/streaming/delta-tracker.ts +33 -0
- package/src/streaming/line-buffer.ts +44 -0
- package/src/streaming/openai-sse.ts +114 -0
- package/src/streaming/parser.ts +22 -0
- package/src/streaming/types.ts +152 -0
- package/src/tools/core/executor.ts +25 -0
- package/src/tools/core/registry.ts +27 -0
- package/src/tools/core/types.ts +31 -0
- package/src/tools/defaults.ts +673 -0
- package/src/tools/discovery.ts +140 -0
- package/src/tools/executors/cli.ts +58 -0
- package/src/tools/executors/local.ts +25 -0
- package/src/tools/executors/mcp.ts +39 -0
- package/src/tools/executors/sdk.ts +39 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/registry.ts +34 -0
- package/src/tools/router.ts +123 -0
- package/src/tools/schema.ts +58 -0
- package/src/tools/skills/loader.ts +61 -0
- package/src/tools/skills/resolver.ts +21 -0
- package/src/tools/types.ts +29 -0
- package/src/types.ts +8 -0
- package/src/utils/errors.ts +131 -0
- package/src/utils/logger.ts +146 -0
- package/src/utils/perf.ts +44 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ToolListResponse } from "@opencode-ai/sdk";
|
|
2
|
+
import { createLogger } from "../utils/logger";
|
|
3
|
+
import stripAnsi from "strip-ansi";
|
|
4
|
+
|
|
5
|
+
const log = createLogger("tools:discovery");
|
|
6
|
+
|
|
7
|
+
export interface OpenCodeTool {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string; // namespaced for OpenAI (e.g., oc_<id>)
|
|
10
|
+
description: string;
|
|
11
|
+
parameters: any; // JSON Schema
|
|
12
|
+
source: "sdk" | "cli" | "mcp";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DiscoveryOptions {
|
|
16
|
+
ttlMs?: number;
|
|
17
|
+
executor?: "sdk" | "cli" | "auto";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class OpenCodeToolDiscovery {
|
|
21
|
+
private client: any;
|
|
22
|
+
private cache: Map<string, OpenCodeTool> = new Map();
|
|
23
|
+
private cacheExpiry = 0;
|
|
24
|
+
private ttl: number;
|
|
25
|
+
private executorPref: "sdk" | "cli" | "auto";
|
|
26
|
+
|
|
27
|
+
constructor(client: any, opts: DiscoveryOptions = {}) {
|
|
28
|
+
this.client = client;
|
|
29
|
+
this.ttl = opts.ttlMs ?? Number(process.env.CURSOR_ACP_TOOL_CACHE_TTL_MS || 60000);
|
|
30
|
+
// Default: auto (SDK first, CLI fallback). Users can force sdk or cli.
|
|
31
|
+
const envPref = process.env.CURSOR_ACP_TOOL_EXECUTOR as any;
|
|
32
|
+
this.executorPref = opts.executor ?? (envPref === "sdk" || envPref === "cli" ? envPref : "auto");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async listTools(): Promise<OpenCodeTool[]> {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
if (this.cache.size > 0 && now < this.cacheExpiry) {
|
|
38
|
+
return Array.from(this.cache.values());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let tools: OpenCodeTool[] = [];
|
|
42
|
+
|
|
43
|
+
// Try SDK first (tool.list) if available
|
|
44
|
+
if (this.executorPref !== "cli" && this.client?.tool?.list) {
|
|
45
|
+
try {
|
|
46
|
+
const resp: ToolListResponse = await this.client.tool.list({});
|
|
47
|
+
const rawTools = Array.isArray(resp?.data) ? resp.data : (resp?.data as any)?.tools || [];
|
|
48
|
+
tools = rawTools.map((t: any) => this.normalize(t, "sdk"));
|
|
49
|
+
|
|
50
|
+
// Merge MCP tools if available on client (best-effort)
|
|
51
|
+
const mcpTools = await this.tryListMcpTools();
|
|
52
|
+
tools = tools.concat(mcpTools);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.debug("SDK tool.list failed, will try CLI", { error: String(err) });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback: CLI opencode tool list --json (only if executorPref allows)
|
|
59
|
+
if (tools.length === 0 && this.executorPref !== "sdk") {
|
|
60
|
+
try {
|
|
61
|
+
const { spawnSync } = await import("node:child_process");
|
|
62
|
+
const cliCmd = process.env.OPENCODE_TOOL_LIST_SHIM
|
|
63
|
+
? process.env.OPENCODE_TOOL_LIST_SHIM.split(" ")
|
|
64
|
+
: ["opencode", "tool", "list", "--json"];
|
|
65
|
+
const res = spawnSync(cliCmd[0], cliCmd.slice(1), { encoding: "utf-8" });
|
|
66
|
+
const parsed = this.parseCliJson(res.stdout || "");
|
|
67
|
+
if (parsed?.data?.tools?.length) {
|
|
68
|
+
tools = parsed.data.tools.map((t: any) => this.normalize(t, "cli"));
|
|
69
|
+
} else {
|
|
70
|
+
log.debug("CLI tool list failed", { status: res.status, stderr: res.stderr });
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log.debug("CLI tool list error", { error: String(err) });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Silent skip if none
|
|
78
|
+
|
|
79
|
+
// Deduplicate by id after namespace
|
|
80
|
+
const map = new Map<string, OpenCodeTool>();
|
|
81
|
+
for (const t of tools) {
|
|
82
|
+
map.set(t.name, t);
|
|
83
|
+
}
|
|
84
|
+
this.cache = map;
|
|
85
|
+
this.cacheExpiry = now + this.ttl;
|
|
86
|
+
return Array.from(this.cache.values());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getToolByName(name: string): OpenCodeTool | undefined {
|
|
90
|
+
return this.cache.get(name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private normalize(t: any, source: "sdk" | "cli" | "mcp"): OpenCodeTool {
|
|
94
|
+
const id = String(t.id || t.name || "unknown");
|
|
95
|
+
const name = this.namespace(id);
|
|
96
|
+
return {
|
|
97
|
+
id,
|
|
98
|
+
name,
|
|
99
|
+
description: String(t.description || "OpenCode tool"),
|
|
100
|
+
parameters: t.parameters || { type: "object", properties: {} },
|
|
101
|
+
source,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private namespace(id: string): string {
|
|
106
|
+
const sanitized = id.replace(/[^a-zA-Z0-9_\-]/g, "_").slice(0, 59); // leave room for prefix
|
|
107
|
+
return `oc_${sanitized}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Best-effort MCP discovery (if SDK exposes it)
|
|
111
|
+
private async tryListMcpTools(): Promise<OpenCodeTool[]> {
|
|
112
|
+
try {
|
|
113
|
+
const mcpList = this.client?.mcp?.tool?.list ? await this.client.mcp.tool.list() : null;
|
|
114
|
+
if (!mcpList?.data?.tools) return [];
|
|
115
|
+
return mcpList.data.tools.map((t: any) => this.normalize(t, "mcp"));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log.debug("MCP tool discovery skipped", { error: String(err) });
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Parse JSON from noisy CLI output (strip ANSI, take last JSON object)
|
|
123
|
+
private parseCliJson(stdout: string): any | null {
|
|
124
|
+
const clean = stripAnsi(stdout || "").trim();
|
|
125
|
+
if (!clean) return null;
|
|
126
|
+
// Fast path
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(clean);
|
|
129
|
+
} catch {}
|
|
130
|
+
// Find last '{'
|
|
131
|
+
const lastBrace = clean.lastIndexOf("{");
|
|
132
|
+
if (lastBrace >= 0) {
|
|
133
|
+
const substr = clean.slice(lastBrace);
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(substr);
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import stripAnsi from "strip-ansi";
|
|
2
|
+
import type { IToolExecutor, ExecutionResult } from "../core/types.js";
|
|
3
|
+
import { createLogger } from "../../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
const log = createLogger("tools:executor:cli");
|
|
6
|
+
|
|
7
|
+
export class CliExecutor implements IToolExecutor {
|
|
8
|
+
constructor(private timeoutMs: number) {}
|
|
9
|
+
|
|
10
|
+
canExecute(): boolean {
|
|
11
|
+
return true; // last-resort; can gate by env if needed
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(toolId: string, args: Record<string, unknown>): Promise<ExecutionResult> {
|
|
15
|
+
try {
|
|
16
|
+
const { spawn } = await import("node:child_process");
|
|
17
|
+
const child = spawn("opencode", ["tool", "run", toolId, "--json", JSON.stringify(args)], {
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const stdoutChunks: Buffer[] = [];
|
|
22
|
+
const stderrChunks: Buffer[] = [];
|
|
23
|
+
|
|
24
|
+
const exited = new Promise<{ code: number | null }>((resolve) => child.on("close", (code) => resolve({ code })));
|
|
25
|
+
|
|
26
|
+
const stdout = new Promise<string>((resolve) => {
|
|
27
|
+
child.stdout?.on("data", (c) => stdoutChunks.push(Buffer.from(c)));
|
|
28
|
+
child.stdout?.on("end", () => resolve(Buffer.concat(stdoutChunks).toString("utf-8")));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const stderr = new Promise<string>((resolve) => {
|
|
32
|
+
child.stderr?.on("data", (c) => stderrChunks.push(Buffer.from(c)));
|
|
33
|
+
child.stderr?.on("end", () => resolve(Buffer.concat(stderrChunks).toString("utf-8")));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const { code } = await this.runWithTimeout(exited);
|
|
37
|
+
const out = await stdout;
|
|
38
|
+
const err = await stderr;
|
|
39
|
+
|
|
40
|
+
if (code === 0) {
|
|
41
|
+
const clean = stripAnsi(out || "");
|
|
42
|
+
return { status: "success", output: clean || "(no output)" };
|
|
43
|
+
}
|
|
44
|
+
return { status: "error", error: stripAnsi(err || out || `Exit code ${code}`) };
|
|
45
|
+
} catch (err: any) {
|
|
46
|
+
log.warn("CLI tool execution failed", { toolId, error: String(err?.message || err) });
|
|
47
|
+
return { status: "error", error: String(err?.message || err) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async runWithTimeout<T>(p: Promise<T>): Promise<T> {
|
|
52
|
+
if (!this.timeoutMs) return p;
|
|
53
|
+
return await Promise.race([
|
|
54
|
+
p,
|
|
55
|
+
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("tool execution timeout")), this.timeoutMs)),
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IToolExecutor, ExecutionResult } from "../core/types.js";
|
|
2
|
+
import type { ToolRegistry } from "../core/registry.js";
|
|
3
|
+
import { createLogger } from "../../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
const log = createLogger("tools:executor:local");
|
|
6
|
+
|
|
7
|
+
export class LocalExecutor implements IToolExecutor {
|
|
8
|
+
constructor(private registry: ToolRegistry) {}
|
|
9
|
+
|
|
10
|
+
canExecute(toolId: string): boolean {
|
|
11
|
+
return Boolean(this.registry.getHandler(toolId));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(toolId: string, args: Record<string, unknown>): Promise<ExecutionResult> {
|
|
15
|
+
const handler = this.registry.getHandler(toolId);
|
|
16
|
+
if (!handler) return { status: "error", error: `Unknown tool ${toolId}` };
|
|
17
|
+
try {
|
|
18
|
+
const out = await handler(args);
|
|
19
|
+
return { status: "success", output: out };
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
log.warn("Local tool execution failed", { toolId, error: String(err?.message || err) });
|
|
22
|
+
return { status: "error", error: String(err?.message || err) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IToolExecutor, ExecutionResult } from "../core/types.js";
|
|
2
|
+
import { createLogger } from "../../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
const log = createLogger("tools:executor:mcp");
|
|
5
|
+
|
|
6
|
+
export class McpExecutor implements IToolExecutor {
|
|
7
|
+
private toolIds = new Set<string>();
|
|
8
|
+
|
|
9
|
+
constructor(private client: any, private timeoutMs: number) {}
|
|
10
|
+
|
|
11
|
+
setToolIds(ids: Iterable<string>): void {
|
|
12
|
+
this.toolIds = new Set(ids);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
canExecute(toolId: string): boolean {
|
|
16
|
+
return Boolean(this.client?.mcp?.tool?.invoke) && this.toolIds.has(toolId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(toolId: string, args: Record<string, unknown>): Promise<ExecutionResult> {
|
|
20
|
+
if (!this.canExecute(toolId)) return { status: "error", error: "MCP invoke unavailable" };
|
|
21
|
+
try {
|
|
22
|
+
const p = this.client.mcp.tool.invoke(toolId, args);
|
|
23
|
+
const res = await this.runWithTimeout(p);
|
|
24
|
+
const out = typeof res === "string" ? res : JSON.stringify(res);
|
|
25
|
+
return { status: "success", output: out };
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
log.warn("MCP tool execution failed", { toolId, error: String(err?.message || err) });
|
|
28
|
+
return { status: "error", error: String(err?.message || err) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async runWithTimeout<T>(p: Promise<T>): Promise<T> {
|
|
33
|
+
if (!this.timeoutMs) return p;
|
|
34
|
+
return await Promise.race([
|
|
35
|
+
p,
|
|
36
|
+
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("tool execution timeout")), this.timeoutMs)),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IToolExecutor, ExecutionResult } from "../core/types.js";
|
|
2
|
+
import { createLogger } from "../../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
const log = createLogger("tools:executor:sdk");
|
|
5
|
+
|
|
6
|
+
export class SdkExecutor implements IToolExecutor {
|
|
7
|
+
private toolIds = new Set<string>();
|
|
8
|
+
|
|
9
|
+
constructor(private client: any, private timeoutMs: number) {}
|
|
10
|
+
|
|
11
|
+
setToolIds(ids: Iterable<string>): void {
|
|
12
|
+
this.toolIds = new Set(ids);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
canExecute(toolId: string): boolean {
|
|
16
|
+
return this.toolIds.has(toolId) && Boolean(this.client?.tool?.invoke);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(toolId: string, args: Record<string, unknown>): Promise<ExecutionResult> {
|
|
20
|
+
if (!this.canExecute(toolId)) return { status: "error", error: "SDK invoke unavailable" };
|
|
21
|
+
try {
|
|
22
|
+
const p = this.client.tool.invoke(toolId, args);
|
|
23
|
+
const res = await this.runWithTimeout(p);
|
|
24
|
+
const out = typeof res === "string" ? res : JSON.stringify(res);
|
|
25
|
+
return { status: "success", output: out };
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
log.warn("SDK tool execution failed", { toolId, error: String(err?.message || err) });
|
|
28
|
+
return { status: "error", error: String(err?.message || err) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async runWithTimeout<T>(p: Promise<T>): Promise<T> {
|
|
33
|
+
if (!this.timeoutMs) return p;
|
|
34
|
+
return await Promise.race([
|
|
35
|
+
p,
|
|
36
|
+
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("tool execution timeout")), this.timeoutMs)),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ToolRegistry } from "./core/registry.js";
|
|
2
|
+
export { executeWithChain } from "./core/executor.js";
|
|
3
|
+
export { registerDefaultTools, getDefaultToolNames } from "./defaults.js";
|
|
4
|
+
export { LocalExecutor } from "./executors/local.js";
|
|
5
|
+
export { SdkExecutor } from "./executors/sdk.js";
|
|
6
|
+
export { McpExecutor } from "./executors/mcp.js";
|
|
7
|
+
export type { ToolDefinition, ToolCall, ToolResult, ToolHandler } from "./types.js";
|
|
8
|
+
export type { ExecutionResult, IToolExecutor, Skill, Tool } from "./core/types.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ToolDefinition, ToolExecutor } from "./types.js";
|
|
2
|
+
|
|
3
|
+
interface RegisteredTool {
|
|
4
|
+
definition: ToolDefinition;
|
|
5
|
+
executor: ToolExecutor;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ToolRegistry {
|
|
9
|
+
private tools: Map<string, RegisteredTool> = new Map();
|
|
10
|
+
|
|
11
|
+
register(name: string, definition: ToolDefinition, executor: ToolExecutor): void {
|
|
12
|
+
this.tools.set(name, { definition, executor });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(name: string): RegisteredTool | undefined {
|
|
16
|
+
return this.tools.get(name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getAllDefinitions(): ToolDefinition[] {
|
|
20
|
+
return Array.from(this.tools.values()).map(t => t.definition);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getExecutor(name: string): ToolExecutor | undefined {
|
|
24
|
+
return this.tools.get(name)?.executor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
has(name: string): boolean {
|
|
28
|
+
return this.tools.has(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getAllToolNames(): string[] {
|
|
32
|
+
return Array.from(this.tools.keys());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger";
|
|
2
|
+
import type { OpenCodeTool } from "./discovery";
|
|
3
|
+
import type { ExecutionResult } from "./core/types.js";
|
|
4
|
+
|
|
5
|
+
const log = createLogger("tools:router");
|
|
6
|
+
|
|
7
|
+
export interface ToolCallEvent {
|
|
8
|
+
type: "tool_call";
|
|
9
|
+
call_id?: string;
|
|
10
|
+
tool_call_id?: string;
|
|
11
|
+
tool_call?: Record<string, { args?: any }>;
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ToolResultChunk {
|
|
16
|
+
id: string;
|
|
17
|
+
object: "chat.completion.chunk";
|
|
18
|
+
created: number;
|
|
19
|
+
model: string;
|
|
20
|
+
choices: Array<{ index: number; delta: any; finish_reason: string | null }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RouterContext {
|
|
24
|
+
execute: (toolId: string, args: Record<string, unknown>) => Promise<ExecutionResult>;
|
|
25
|
+
toolsByName: Map<string, OpenCodeTool>;
|
|
26
|
+
resolveName?: (name: string) => string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ToolRouter {
|
|
30
|
+
private ctx: RouterContext;
|
|
31
|
+
|
|
32
|
+
constructor(ctx: RouterContext) {
|
|
33
|
+
this.ctx = ctx;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isOpenCodeTool(name: string | undefined): boolean {
|
|
37
|
+
return !!name && name.startsWith("oc_");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async handleToolCall(event: ToolCallEvent, meta: { id: string; created: number; model: string }): Promise<ToolResultChunk | null> {
|
|
41
|
+
const callId = event.call_id || event.tool_call_id || "unknown";
|
|
42
|
+
let name = event.name || this.inferName(event);
|
|
43
|
+
if (!this.isOpenCodeTool(name)) return null;
|
|
44
|
+
|
|
45
|
+
// Resolve aliases via SkillResolver if configured
|
|
46
|
+
if (this.ctx.resolveName) {
|
|
47
|
+
const resolved = this.ctx.resolveName(name);
|
|
48
|
+
if (resolved) {
|
|
49
|
+
name = resolved;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tool = this.ctx.toolsByName.get(name);
|
|
54
|
+
if (!tool) {
|
|
55
|
+
log.warn("Unknown tool call", { name });
|
|
56
|
+
return this.buildResult(meta, callId, name, { status: "error", error: `Unknown tool ${name}` });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const args = this.extractArgs(event);
|
|
60
|
+
log.debug("Executing tool", { name, toolId: tool.id });
|
|
61
|
+
const t0 = Date.now();
|
|
62
|
+
const result = await this.ctx.execute(tool.id, args);
|
|
63
|
+
const elapsed = Date.now() - t0;
|
|
64
|
+
if (result.status === "error") {
|
|
65
|
+
log.warn("Tool execution returned error", { name, error: result.error, elapsed });
|
|
66
|
+
} else {
|
|
67
|
+
log.debug("Tool execution completed", { name, toolId: tool.id, elapsed });
|
|
68
|
+
}
|
|
69
|
+
return this.buildResult(meta, callId, name, result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private extractArgs(event: ToolCallEvent): any {
|
|
73
|
+
if (event.tool_call) {
|
|
74
|
+
const [key] = Object.keys(event.tool_call);
|
|
75
|
+
return event.tool_call[key]?.args || {};
|
|
76
|
+
}
|
|
77
|
+
// Some agents send args at top-level
|
|
78
|
+
return (event as any).arguments || {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private inferName(event: ToolCallEvent): string | undefined {
|
|
82
|
+
if (event.tool_call) {
|
|
83
|
+
const [key] = Object.keys(event.tool_call);
|
|
84
|
+
return key;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private buildResult(meta: { id: string; created: number; model: string }, callId: string, name: string, result: { status: string; output?: string; error?: string }): ToolResultChunk {
|
|
90
|
+
const delta: any = {
|
|
91
|
+
role: "assistant",
|
|
92
|
+
tool_calls: [
|
|
93
|
+
{
|
|
94
|
+
id: callId,
|
|
95
|
+
type: "function",
|
|
96
|
+
function: {
|
|
97
|
+
name,
|
|
98
|
+
arguments: "{}",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// OpenAI tool result convention: include output in a message? We'll place in a synthetic "content" string.
|
|
105
|
+
const content = result.status === "success" ? result.output ?? "" : (result.error || "unknown error");
|
|
106
|
+
|
|
107
|
+
delta.tool_calls[0].function.arguments = JSON.stringify({ result: content }).slice(0, 8000); // guard size
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
id: meta.id,
|
|
111
|
+
object: "chat.completion.chunk",
|
|
112
|
+
created: meta.created,
|
|
113
|
+
model: meta.model,
|
|
114
|
+
choices: [
|
|
115
|
+
{
|
|
116
|
+
index: 0,
|
|
117
|
+
delta,
|
|
118
|
+
finish_reason: null,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger";
|
|
2
|
+
|
|
3
|
+
const log = createLogger("tools:schema");
|
|
4
|
+
|
|
5
|
+
// Convert JSON Schema from OpenCode to OpenAI function parameters shape
|
|
6
|
+
export function toOpenAiParameters(schema: any): any {
|
|
7
|
+
if (!schema || typeof schema !== "object") {
|
|
8
|
+
return { type: "object", properties: {} };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const clone = (obj: any): any => {
|
|
12
|
+
if (Array.isArray(obj)) return obj.map(clone);
|
|
13
|
+
if (obj && typeof obj === "object") {
|
|
14
|
+
const out: any = {};
|
|
15
|
+
for (const k of Object.keys(obj)) {
|
|
16
|
+
out[k] = clone(obj[k]);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
return obj;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const cleaned = clone(schema);
|
|
24
|
+
|
|
25
|
+
// Strip unsupported keywords that can confuse OpenAI tools
|
|
26
|
+
const stripKeys = ["additionalProperties", "$schema", "$id", "unevaluatedProperties", "definitions", "$defs"];
|
|
27
|
+
const walk = (node: any) => {
|
|
28
|
+
if (!node || typeof node !== "object") return;
|
|
29
|
+
for (const key of stripKeys) {
|
|
30
|
+
if (key in node) delete node[key];
|
|
31
|
+
}
|
|
32
|
+
if (node.properties) {
|
|
33
|
+
for (const k of Object.keys(node.properties)) {
|
|
34
|
+
walk(node.properties[k]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (node.items) walk(node.items);
|
|
38
|
+
if (Array.isArray(node.anyOf)) node.anyOf.forEach(walk);
|
|
39
|
+
if (Array.isArray(node.oneOf)) node.oneOf.forEach(walk);
|
|
40
|
+
if (Array.isArray(node.allOf)) node.allOf.forEach(walk);
|
|
41
|
+
};
|
|
42
|
+
walk(cleaned);
|
|
43
|
+
|
|
44
|
+
// Ensure top-level object
|
|
45
|
+
if (cleaned.type !== "object") {
|
|
46
|
+
cleaned.type = "object";
|
|
47
|
+
if (!cleaned.properties) cleaned.properties = {};
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(cleaned.required)) cleaned.required = [];
|
|
50
|
+
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function describeTool(t: { id: string; description?: string }): string {
|
|
55
|
+
const base = t.description || "OpenCode tool";
|
|
56
|
+
// Keep concise
|
|
57
|
+
return base.length > 400 ? base.slice(0, 400) : base;
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OpenCodeTool } from "../discovery.js";
|
|
2
|
+
import type { Skill } from "../core/types.js";
|
|
3
|
+
|
|
4
|
+
function deriveCategory(name: string): string | undefined {
|
|
5
|
+
if (!name) return undefined;
|
|
6
|
+
const segments = name.split(/[\/:]/).filter(Boolean);
|
|
7
|
+
if (segments.length === 0) return undefined;
|
|
8
|
+
// Prefer last segment as the skill topic (e.g., superpowers/brainstorming -> brainstorming)
|
|
9
|
+
return segments[segments.length - 1].toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function deriveTriggers(name: string, description?: string): string[] {
|
|
13
|
+
const words = new Set<string>();
|
|
14
|
+
const addWord = (w: string) => {
|
|
15
|
+
const word = w.toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
16
|
+
if (word.length >= 4) words.add(word);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
name.split(/[\s\/:_-]+/).forEach(addWord);
|
|
20
|
+
(description || "")
|
|
21
|
+
.split(/[\s,.;:()]+/)
|
|
22
|
+
.filter((w) => w.length >= 4)
|
|
23
|
+
.slice(0, 12)
|
|
24
|
+
.forEach(addWord);
|
|
25
|
+
|
|
26
|
+
return Array.from(words).slice(0, 6);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SkillLoader {
|
|
30
|
+
load(tools: OpenCodeTool[]): Skill[] {
|
|
31
|
+
return tools.map((t) => {
|
|
32
|
+
const baseId = t.id.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
33
|
+
const aliases = [
|
|
34
|
+
t.name,
|
|
35
|
+
baseId,
|
|
36
|
+
`oc_${baseId}`,
|
|
37
|
+
`oc_skill_${baseId}`,
|
|
38
|
+
`oc_superskill_${baseId}`,
|
|
39
|
+
`oc_superpowers_${baseId}`,
|
|
40
|
+
];
|
|
41
|
+
if (t.name === "todowrite") {
|
|
42
|
+
aliases.push("updateTodos", "updateTodosToolCall", "todoWrite", "todoWriteToolCall");
|
|
43
|
+
}
|
|
44
|
+
if (t.name === "todoread") {
|
|
45
|
+
aliases.push("readTodos", "readTodosToolCall", "todoRead", "todoReadToolCall");
|
|
46
|
+
}
|
|
47
|
+
const category = deriveCategory(t.name);
|
|
48
|
+
const triggers = deriveTriggers(t.name, t.description);
|
|
49
|
+
return {
|
|
50
|
+
id: t.id,
|
|
51
|
+
name: t.name,
|
|
52
|
+
description: t.description,
|
|
53
|
+
parameters: t.parameters,
|
|
54
|
+
source: t.source,
|
|
55
|
+
aliases,
|
|
56
|
+
category,
|
|
57
|
+
triggers,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Skill } from "../core/types.js";
|
|
2
|
+
|
|
3
|
+
export class SkillResolver {
|
|
4
|
+
private aliasToName = new Map<string, string>();
|
|
5
|
+
|
|
6
|
+
constructor(skills: Skill[]) {
|
|
7
|
+
for (const s of skills) {
|
|
8
|
+
const aliases = new Set<string>([(s.name || "").toLowerCase(), (s.id || "").toLowerCase()]);
|
|
9
|
+
(s.aliases || []).forEach((a) => aliases.add(a.toLowerCase()));
|
|
10
|
+
(s.triggers || []).forEach((t) => aliases.add(t.toLowerCase()));
|
|
11
|
+
for (const a of aliases) {
|
|
12
|
+
this.aliasToName.set(a, s.name);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resolve(name?: string): string | undefined {
|
|
18
|
+
if (!name) return undefined;
|
|
19
|
+
return this.aliasToName.get(name.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ToolDefinition {
|
|
2
|
+
type: "function";
|
|
3
|
+
function: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object";
|
|
8
|
+
properties: Record<string, any>;
|
|
9
|
+
required?: string[];
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolCall {
|
|
15
|
+
id: string;
|
|
16
|
+
type: "function";
|
|
17
|
+
function: {
|
|
18
|
+
name: string;
|
|
19
|
+
arguments: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ToolResult {
|
|
24
|
+
tool_call_id: string;
|
|
25
|
+
role: "tool";
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ToolHandler = (args: any) => Promise<string>;
|