@oh-my-tool/cli 0.2.0 → 0.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/README.md +7 -2
- package/assets/skills/oh-my-tool/SKILL.md +11 -3
- package/bin/ohmytool.cjs +0 -0
- package/package.json +12 -10
- package/src/cli/commands/describe.ts +23 -22
- package/src/cli/commands/extension.ts +24 -24
- package/src/cli/commands/index.ts +7 -6
- package/src/cli/commands/integrate.ts +64 -64
- package/src/cli/commands/mcp.ts +86 -0
- package/src/cli/commands/run.ts +8 -7
- package/src/cli/commands/search.ts +14 -13
- package/src/cli/commands/secret.ts +68 -68
- package/src/cli/context.ts +25 -2
- package/src/cli/index.ts +296 -272
- package/src/cli/parseArgs.ts +62 -44
- package/src/config/config.ts +155 -63
- package/src/core/executor.ts +89 -89
- package/src/core/registry.ts +31 -31
- package/src/core/result.ts +14 -14
- package/src/extension/discovery.ts +61 -61
- package/src/extension/install.ts +23 -23
- package/src/extension/loader.ts +32 -32
- package/src/extension/manifest.ts +114 -114
- package/src/integration/adapters.ts +98 -98
- package/src/integration/index.ts +4 -4
- package/src/integration/manager.ts +375 -375
- package/src/integration/skill.ts +84 -84
- package/src/integration/types.ts +55 -55
- package/src/policy/policy.ts +136 -136
- package/src/runtime/errors.ts +7 -2
- package/src/runtime/executor.ts +6 -1
- package/src/runtime/provider.ts +1 -0
- package/src/runtime/providers/mcp/normalize.ts +36 -0
- package/src/runtime/providers/mcp/oauth-callback.ts +91 -0
- package/src/runtime/providers/mcp/oauth-provider.ts +348 -0
- package/src/runtime/providers/mcp/oauth-store.ts +106 -0
- package/src/runtime/providers/mcp/provider.ts +99 -0
- package/src/runtime/providers/mcp/safe-errors.ts +63 -0
- package/src/runtime/providers/mcp/session.ts +117 -0
- package/src/runtime/providers/mcp/transport.ts +140 -0
- package/src/runtime/result.ts +1 -1
- package/src/runtime/runtime.ts +38 -12
- package/src/runtime/schema.ts +14 -4
- package/src/search/search.ts +78 -78
- package/src/secrets/secrets.ts +45 -45
- package/src/version.ts +1 -1
package/src/cli/parseArgs.ts
CHANGED
|
@@ -1,44 +1,62 @@
|
|
|
1
|
-
export interface ParsedArgs {
|
|
2
|
-
positional: string[];
|
|
3
|
-
keyValues: Record<string, string>;
|
|
4
|
-
flags: string[];
|
|
5
|
-
options: Record<string, string>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
export interface ParsedArgs {
|
|
2
|
+
positional: string[];
|
|
3
|
+
keyValues: Record<string, string>;
|
|
4
|
+
flags: string[];
|
|
5
|
+
options: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ParsedMcpCommand =
|
|
9
|
+
| { readonly action: "list" }
|
|
10
|
+
| { readonly action: "auth" | "logout"; readonly serverId: string };
|
|
11
|
+
|
|
12
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
13
|
+
const positional: string[] = [];
|
|
14
|
+
const keyValues: Record<string, string> = {};
|
|
15
|
+
const flags: string[] = [];
|
|
16
|
+
const options: Record<string, string> = {};
|
|
17
|
+
for (const arg of argv) {
|
|
18
|
+
if (arg.startsWith("--")) {
|
|
19
|
+
const option = arg.slice(2);
|
|
20
|
+
const eq = option.indexOf("=");
|
|
21
|
+
if (eq > 0) {
|
|
22
|
+
options[option.slice(0, eq)] = option.slice(eq + 1);
|
|
23
|
+
} else {
|
|
24
|
+
flags.push(option);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
const eq = arg.indexOf("=");
|
|
28
|
+
if (eq > 0) {
|
|
29
|
+
keyValues[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
30
|
+
} else {
|
|
31
|
+
positional.push(arg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { positional, keyValues, flags, options };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseMcpCommand(args: ParsedArgs): ParsedMcpCommand | undefined {
|
|
39
|
+
if (args.positional[0] !== "mcp") return undefined;
|
|
40
|
+
const action = args.positional[1];
|
|
41
|
+
const serverId = args.positional[2];
|
|
42
|
+
if (action === "list") return args.positional.length === 2 ? { action } : undefined;
|
|
43
|
+
if (
|
|
44
|
+
(action !== "auth" && action !== "logout") ||
|
|
45
|
+
serverId === undefined ||
|
|
46
|
+
serverId.length === 0 ||
|
|
47
|
+
args.positional.length !== 3
|
|
48
|
+
) return undefined;
|
|
49
|
+
return { action, serverId };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function coerceInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
53
|
+
const out: Record<string, unknown> = {};
|
|
54
|
+
for (const [k, v] of Object.entries(input)) {
|
|
55
|
+
if (typeof v === "string" && /^-?\d+$/.test(v)) {
|
|
56
|
+
out[k] = Number(v);
|
|
57
|
+
} else {
|
|
58
|
+
out[k] = v;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
package/src/config/config.ts
CHANGED
|
@@ -1,63 +1,155 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { RuntimeError } from "../runtime/errors";
|
|
4
|
+
|
|
5
|
+
export interface ConnectionConfig {
|
|
6
|
+
environment: string;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
database: string;
|
|
10
|
+
username: string;
|
|
11
|
+
secret: string;
|
|
12
|
+
tls: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface McpCommonServerConfig { readonly enabled: true; readonly namespace: string; }
|
|
16
|
+
export interface McpStdioServerConfig extends McpCommonServerConfig {
|
|
17
|
+
readonly transport: "stdio"; readonly command: string; readonly args: readonly string[]; readonly cwd?: string;
|
|
18
|
+
readonly env: Readonly<Record<string, string>>; readonly secretEnv: Readonly<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
export interface McpHttpServerConfig extends McpCommonServerConfig {
|
|
21
|
+
readonly transport: "streamable-http"; readonly url: string; readonly headers: Readonly<Record<string, string>>;
|
|
22
|
+
readonly secretHeaders: Readonly<Record<string, string>>; readonly auth: McpHttpAuthConfig;
|
|
23
|
+
}
|
|
24
|
+
export type McpHttpAuthConfig =
|
|
25
|
+
| { readonly type: "none" }
|
|
26
|
+
| { readonly type: "bearer"; readonly tokenSecret: string }
|
|
27
|
+
| { readonly type: "oauth"; readonly scopes: readonly string[]; readonly callbackPort: number; readonly clientId?: string; readonly clientSecretSecret?: string; readonly tokenEndpointAuthMethod: "none" | "client_secret_basic" | "client_secret_post" };
|
|
28
|
+
export type McpEnabledServerConfig = McpStdioServerConfig | McpHttpServerConfig;
|
|
29
|
+
export interface McpDisabledServerConfig {
|
|
30
|
+
readonly enabled: false;
|
|
31
|
+
readonly namespace: string;
|
|
32
|
+
readonly transport: "disabled";
|
|
33
|
+
}
|
|
34
|
+
export type McpServerConfig = McpEnabledServerConfig | McpDisabledServerConfig;
|
|
35
|
+
export interface Config {
|
|
36
|
+
extensions: Record<string, { connections: Record<string, ConnectionConfig> }>;
|
|
37
|
+
mcp: { servers: Record<string, McpServerConfig> };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function invalid(path: string, reason: string): never { throw new RuntimeError("MCP_INVALID_CONFIG", `${path}: ${reason}`); }
|
|
41
|
+
|
|
42
|
+
function parseStringMap(value: unknown, path: string): Record<string, string> {
|
|
43
|
+
if (value === undefined) return {};
|
|
44
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) invalid(path, "must be a table");
|
|
45
|
+
const result: Record<string, string> = {};
|
|
46
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
47
|
+
if (typeof entry !== "string") invalid(`${path}.${key}`, "must be a string");
|
|
48
|
+
result[key] = entry;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseStringArray(value: unknown, path: string): string[] {
|
|
54
|
+
if (value === undefined) return [];
|
|
55
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) invalid(path, "must be an array of strings");
|
|
56
|
+
return [...value];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertMcpName(value: string, path: string): void {
|
|
60
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(value)) {
|
|
61
|
+
invalid(path, "must contain only lowercase letters, numbers, underscores, and hyphens");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requiredString(value: unknown, path: string): string {
|
|
66
|
+
if (typeof value !== "string" || value.length === 0) invalid(path, "must be a non-empty string");
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseMcpServer(id: string, value: unknown): McpServerConfig {
|
|
71
|
+
const path = `mcp.servers.${id}`;
|
|
72
|
+
assertMcpName(id, path);
|
|
73
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) invalid(path, "must be a table");
|
|
74
|
+
const raw = value as Record<string, unknown>;
|
|
75
|
+
const enabled = raw.enabled === undefined ? true : raw.enabled;
|
|
76
|
+
if (typeof enabled !== "boolean") invalid(`${path}.enabled`, "must be a boolean");
|
|
77
|
+
if (!enabled) return { enabled: false, namespace: id, transport: "disabled" };
|
|
78
|
+
const namespace = raw.namespace === undefined ? id : requiredString(raw.namespace, `${path}.namespace`);
|
|
79
|
+
assertMcpName(namespace, `${path}.namespace`);
|
|
80
|
+
if (namespace === "native" || namespace === "mcp") invalid(`${path}.namespace`, "is reserved");
|
|
81
|
+
|
|
82
|
+
if (raw.transport === "stdio") {
|
|
83
|
+
if (raw.auth !== undefined) invalid(`${path}.auth`, "is only supported for HTTP servers");
|
|
84
|
+
const command = requiredString(raw.command, `${path}.command`);
|
|
85
|
+
const args = parseStringArray(raw.args, `${path}.args`);
|
|
86
|
+
const cwd = raw.cwd === undefined ? undefined : requiredString(raw.cwd, `${path}.cwd`);
|
|
87
|
+
return { enabled, transport: "stdio", command, args, ...(cwd === undefined ? {} : { cwd }), namespace, env: parseStringMap(raw.env, `${path}.env`), secretEnv: parseStringMap(raw.secretEnv, `${path}.secretEnv`) };
|
|
88
|
+
}
|
|
89
|
+
if (raw.transport !== "streamable-http") invalid(`${path}.transport`, "must be stdio or streamable-http");
|
|
90
|
+
const url = requiredString(raw.url, `${path}.url`);
|
|
91
|
+
try {
|
|
92
|
+
const parsedUrl = new URL(url);
|
|
93
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") invalid(`${path}.url`, "must use http or https");
|
|
94
|
+
} catch { invalid(`${path}.url`, "must be a valid HTTP(S) URL"); }
|
|
95
|
+
const headers = parseStringMap(raw.headers, `${path}.headers`);
|
|
96
|
+
const secretHeaders = parseStringMap(raw.secretHeaders, `${path}.secretHeaders`);
|
|
97
|
+
const secretHeaderNames = new Set(Object.keys(secretHeaders).map((key) => key.toLowerCase()));
|
|
98
|
+
for (const key of Object.keys(headers)) {
|
|
99
|
+
if (secretHeaderNames.has(key.toLowerCase())) invalid(`${path}.headers`, "must not overlap secretHeaders");
|
|
100
|
+
}
|
|
101
|
+
const hasAuthorizationHeader = [...Object.keys(headers), ...Object.keys(secretHeaders)]
|
|
102
|
+
.some((key) => key.toLowerCase() === "authorization");
|
|
103
|
+
if (hasAuthorizationHeader && raw.bearerTokenSecret !== undefined) {
|
|
104
|
+
invalid(`${path}.bearerTokenSecret`, "must not be combined with Authorization");
|
|
105
|
+
}
|
|
106
|
+
const authMode = raw.auth === undefined ? "none" : raw.auth;
|
|
107
|
+
if (hasAuthorizationHeader && (authMode === "bearer" || authMode === "oauth")) {
|
|
108
|
+
invalid(`${path}.headers`, "must not configure Authorization when bearer or oauth auth is enabled");
|
|
109
|
+
}
|
|
110
|
+
if (authMode === "none") return { enabled, transport: "streamable-http", url, namespace, headers, secretHeaders, auth: { type: "none" } };
|
|
111
|
+
if (authMode === "bearer") return { enabled, transport: "streamable-http", url, namespace, headers, secretHeaders, auth: { type: "bearer", tokenSecret: requiredString(raw.bearerTokenSecret, `${path}.bearerTokenSecret`) } };
|
|
112
|
+
if (authMode !== "oauth") invalid(`${path}.auth`, "must be none, bearer, or oauth");
|
|
113
|
+
const scopes = parseStringArray(raw.oauthScopes, `${path}.oauthScopes`);
|
|
114
|
+
const callbackPort = raw.oauthCallbackPort === undefined ? 0 : raw.oauthCallbackPort;
|
|
115
|
+
if (typeof callbackPort !== "number" || !Number.isInteger(callbackPort) || (callbackPort !== 0 && (callbackPort < 1024 || callbackPort > 65535))) invalid(`${path}.oauthCallbackPort`, "must be 0 or an integer from 1024 through 65535");
|
|
116
|
+
const clientId = raw.oauthClientId === undefined ? undefined : requiredString(raw.oauthClientId, `${path}.oauthClientId`);
|
|
117
|
+
const clientSecretSecret = raw.oauthClientSecretSecret === undefined ? undefined : requiredString(raw.oauthClientSecretSecret, `${path}.oauthClientSecretSecret`);
|
|
118
|
+
if (clientSecretSecret !== undefined && clientId === undefined) invalid(`${path}.oauthClientSecretSecret`, "requires oauthClientId");
|
|
119
|
+
const tokenEndpointAuthMethod = raw.oauthTokenEndpointAuthMethod === undefined ? "none" : raw.oauthTokenEndpointAuthMethod;
|
|
120
|
+
if (tokenEndpointAuthMethod !== "none" && tokenEndpointAuthMethod !== "client_secret_basic" && tokenEndpointAuthMethod !== "client_secret_post") invalid(`${path}.oauthTokenEndpointAuthMethod`, "must be none, client_secret_basic, or client_secret_post");
|
|
121
|
+
if (tokenEndpointAuthMethod !== "none" && clientSecretSecret === undefined) invalid(`${path}.oauthTokenEndpointAuthMethod`, "requires oauthClientSecretSecret");
|
|
122
|
+
if (tokenEndpointAuthMethod === "none" && clientSecretSecret !== undefined) invalid(`${path}.oauthTokenEndpointAuthMethod`, "cannot be none with a client secret");
|
|
123
|
+
return { enabled, transport: "streamable-http", url, namespace, headers, secretHeaders, auth: { type: "oauth", scopes, callbackPort, ...(clientId === undefined ? {} : { clientId }), ...(clientSecretSecret === undefined ? {} : { clientSecretSecret }), tokenEndpointAuthMethod } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function loadConfig(homeDir: string): Config {
|
|
127
|
+
const path = join(homeDir, "config.toml");
|
|
128
|
+
if (!existsSync(path)) return { extensions: {}, mcp: { servers: {} } };
|
|
129
|
+
const parsed = Bun.TOML.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
130
|
+
const extensions: Config["extensions"] = {};
|
|
131
|
+
const extSection = parsed.extensions;
|
|
132
|
+
if (extSection && typeof extSection === "object") for (const [extId, extVal] of Object.entries(extSection)) {
|
|
133
|
+
const connections: Record<string, ConnectionConfig> = {};
|
|
134
|
+
const connSection = (extVal as Record<string, any>)?.connections;
|
|
135
|
+
if (connSection && typeof connSection === "object") for (const [name, rawConn] of Object.entries(connSection)) {
|
|
136
|
+
const rc = rawConn as Record<string, any>;
|
|
137
|
+
connections[name] = { environment: String(rc.environment ?? ""), host: String(rc.host ?? ""), port: Number(rc.port ?? 3306), database: String(rc.database ?? ""), username: String(rc.username ?? ""), secret: String(rc.secret ?? ""), tls: Boolean(rc.tls ?? false) };
|
|
138
|
+
}
|
|
139
|
+
extensions[extId] = { connections };
|
|
140
|
+
}
|
|
141
|
+
const servers: Record<string, McpServerConfig> = {};
|
|
142
|
+
const serverSection = (parsed.mcp as Record<string, unknown> | undefined)?.servers;
|
|
143
|
+
if (serverSection !== undefined) {
|
|
144
|
+
if (serverSection === null || typeof serverSection !== "object" || Array.isArray(serverSection)) {
|
|
145
|
+
invalid("mcp.servers", "must be a table");
|
|
146
|
+
}
|
|
147
|
+
for (const [id, value] of Object.entries(serverSection)) {
|
|
148
|
+
servers[id] = parseMcpServer(id, value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { extensions, mcp: { servers } };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getConnectionConfig(cfg: Config, extensionId: string, connection: string): ConnectionConfig | undefined { return cfg.extensions[extensionId]?.connections[connection]; }
|
|
155
|
+
export function listConnections(cfg: Config, extensionId: string): string[] { return Object.keys(cfg.extensions[extensionId]?.connections ?? {}); }
|
package/src/core/executor.ts
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
import type { Logger, SecretStore, ToolContext, ToolResult } from "@oh-my-tool/sdk";
|
|
2
|
-
import { ToolError } from "@oh-my-tool/sdk";
|
|
3
|
-
import type { Config } from "../config/config";
|
|
4
|
-
import { getConnectionConfig } from "../config/config";
|
|
5
|
-
import { resolveTool, OmtError, type Registry } from "./registry";
|
|
6
|
-
import { validateInput, type Schema } from "./schema";
|
|
7
|
-
import { validateConnectionInput, applyLimits, PolicyError } from "../policy/policy";
|
|
8
|
-
import { loadExtension } from "../extension/loader";
|
|
9
|
-
import type { OmtResult } from "./result";
|
|
10
|
-
|
|
11
|
-
const noopLogger: Logger = {
|
|
12
|
-
debug: () => {},
|
|
13
|
-
info: () => {},
|
|
14
|
-
warn: () => {},
|
|
15
|
-
error: () => {},
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export interface ExecutorDeps {
|
|
19
|
-
registry: Registry;
|
|
20
|
-
config: Config;
|
|
21
|
-
secrets: SecretStore;
|
|
22
|
-
logger?: Logger;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function hasConnection(schema: Schema | undefined): boolean {
|
|
26
|
-
return Boolean(schema?.properties && "connection" in schema.properties);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function executeTool(
|
|
30
|
-
deps: ExecutorDeps,
|
|
31
|
-
toolName: string,
|
|
32
|
-
rawInput: Record<string, unknown>,
|
|
33
|
-
): Promise<OmtResult> {
|
|
34
|
-
const started = Date.now();
|
|
35
|
-
try {
|
|
36
|
-
const { extension, tool } = resolveTool(deps.registry, toolName);
|
|
37
|
-
const schema = tool.inputSchema as Schema | undefined;
|
|
38
|
-
|
|
39
|
-
const limits = applyLimits(rawInput);
|
|
40
|
-
const normalized = { ...rawInput, maxRows: limits.maxRows, timeoutMs: limits.timeoutMs };
|
|
41
|
-
|
|
42
|
-
const needsConnection = hasConnection(schema) || "connection" in normalized;
|
|
43
|
-
if (needsConnection) {
|
|
44
|
-
validateConnectionInput(normalized, deps.config, extension.id);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const input = validateInput(schema, normalized);
|
|
48
|
-
|
|
49
|
-
const connectionCfg = needsConnection
|
|
50
|
-
? getConnectionConfig(deps.config, extension.id, String(input.connection))
|
|
51
|
-
: undefined;
|
|
52
|
-
|
|
53
|
-
const ctx: ToolContext = {
|
|
54
|
-
toolName,
|
|
55
|
-
logger: deps.logger ?? noopLogger,
|
|
56
|
-
config: (connectionCfg ?? {}) as Record<string, unknown>,
|
|
57
|
-
secrets: deps.secrets,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const def = await loadExtension(extension);
|
|
61
|
-
const handler = def.handlers[toolName];
|
|
62
|
-
if (!handler) {
|
|
63
|
-
throw new OmtError("HANDLER_MISSING", `no handler for ${toolName}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const result: ToolResult = await handler(ctx, input);
|
|
67
|
-
const durationMs = Date.now() - started;
|
|
68
|
-
return {
|
|
69
|
-
ok: true,
|
|
70
|
-
tool: toolName,
|
|
71
|
-
data: result.data,
|
|
72
|
-
meta: { durationMs, ...(result.meta ?? {}) },
|
|
73
|
-
};
|
|
74
|
-
} catch (e) {
|
|
75
|
-
const durationMs = Date.now() - started;
|
|
76
|
-
if (e instanceof PolicyError) {
|
|
77
|
-
return { ok: false, tool: toolName, error: { code: "POLICY_VIOLATION", message: e.message } };
|
|
78
|
-
}
|
|
79
|
-
if (e instanceof ToolError) {
|
|
80
|
-
return { ok: false, tool: toolName, error: { code: e.code, message: e.message } };
|
|
81
|
-
}
|
|
1
|
+
import type { Logger, SecretStore, ToolContext, ToolResult } from "@oh-my-tool/sdk";
|
|
2
|
+
import { ToolError } from "@oh-my-tool/sdk";
|
|
3
|
+
import type { Config } from "../config/config";
|
|
4
|
+
import { getConnectionConfig } from "../config/config";
|
|
5
|
+
import { resolveTool, OmtError, type Registry } from "./registry";
|
|
6
|
+
import { validateInput, type Schema } from "./schema";
|
|
7
|
+
import { validateConnectionInput, applyLimits, PolicyError } from "../policy/policy";
|
|
8
|
+
import { loadExtension } from "../extension/loader";
|
|
9
|
+
import type { OmtResult } from "./result";
|
|
10
|
+
|
|
11
|
+
const noopLogger: Logger = {
|
|
12
|
+
debug: () => {},
|
|
13
|
+
info: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
error: () => {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface ExecutorDeps {
|
|
19
|
+
registry: Registry;
|
|
20
|
+
config: Config;
|
|
21
|
+
secrets: SecretStore;
|
|
22
|
+
logger?: Logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasConnection(schema: Schema | undefined): boolean {
|
|
26
|
+
return Boolean(schema?.properties && "connection" in schema.properties);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function executeTool(
|
|
30
|
+
deps: ExecutorDeps,
|
|
31
|
+
toolName: string,
|
|
32
|
+
rawInput: Record<string, unknown>,
|
|
33
|
+
): Promise<OmtResult> {
|
|
34
|
+
const started = Date.now();
|
|
35
|
+
try {
|
|
36
|
+
const { extension, tool } = resolveTool(deps.registry, toolName);
|
|
37
|
+
const schema = tool.inputSchema as Schema | undefined;
|
|
38
|
+
|
|
39
|
+
const limits = applyLimits(rawInput);
|
|
40
|
+
const normalized = { ...rawInput, maxRows: limits.maxRows, timeoutMs: limits.timeoutMs };
|
|
41
|
+
|
|
42
|
+
const needsConnection = hasConnection(schema) || "connection" in normalized;
|
|
43
|
+
if (needsConnection) {
|
|
44
|
+
validateConnectionInput(normalized, deps.config, extension.id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const input = validateInput(schema, normalized);
|
|
48
|
+
|
|
49
|
+
const connectionCfg = needsConnection
|
|
50
|
+
? getConnectionConfig(deps.config, extension.id, String(input.connection))
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
const ctx: ToolContext = {
|
|
54
|
+
toolName,
|
|
55
|
+
logger: deps.logger ?? noopLogger,
|
|
56
|
+
config: (connectionCfg ?? {}) as Record<string, unknown>,
|
|
57
|
+
secrets: deps.secrets,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const def = await loadExtension(extension);
|
|
61
|
+
const handler = def.handlers[toolName];
|
|
62
|
+
if (!handler) {
|
|
63
|
+
throw new OmtError("HANDLER_MISSING", `no handler for ${toolName}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result: ToolResult = await handler(ctx, input);
|
|
67
|
+
const durationMs = Date.now() - started;
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
tool: toolName,
|
|
71
|
+
data: result.data,
|
|
72
|
+
meta: { durationMs, ...(result.meta ?? {}) },
|
|
73
|
+
};
|
|
74
|
+
} catch (e) {
|
|
75
|
+
const durationMs = Date.now() - started;
|
|
76
|
+
if (e instanceof PolicyError) {
|
|
77
|
+
return { ok: false, tool: toolName, error: { code: "POLICY_VIOLATION", message: e.message } };
|
|
78
|
+
}
|
|
79
|
+
if (e instanceof ToolError) {
|
|
80
|
+
return { ok: false, tool: toolName, error: { code: e.code, message: e.message } };
|
|
81
|
+
}
|
|
82
82
|
if (e instanceof OmtError) {
|
|
83
83
|
return { ok: false, tool: toolName, error: { code: e.code, message: e.message } };
|
|
84
84
|
}
|
|
@@ -89,11 +89,11 @@ export async function executeTool(
|
|
|
89
89
|
error: { code: (e as { code: string }).code, message: e instanceof Error ? e.message : String(e) },
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
tool: toolName,
|
|
95
|
-
error: { code: "EXECUTION_FAILED", message: e instanceof Error ? e.message : String(e) },
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
tool: toolName,
|
|
95
|
+
error: { code: "EXECUTION_FAILED", message: e instanceof Error ? e.message : String(e) },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
package/src/core/registry.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import type { ExtensionManifest } from "@oh-my-tool/sdk";
|
|
2
|
-
import type { InstalledExtension } from "../extension/discovery";
|
|
3
|
-
|
|
1
|
+
import type { ExtensionManifest } from "@oh-my-tool/sdk";
|
|
2
|
+
import type { InstalledExtension } from "../extension/discovery";
|
|
3
|
+
|
|
4
4
|
import { RuntimeError as OmtError } from "../runtime/errors";
|
|
5
5
|
export { OmtError };
|
|
6
|
-
|
|
7
|
-
export interface Registry {
|
|
8
|
-
byTool: Map<string, { extension: InstalledExtension; tool: ExtensionManifest["tools"][number] }>;
|
|
9
|
-
byId: Map<string, InstalledExtension>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createRegistry(installed: InstalledExtension[]): Registry {
|
|
13
|
-
const byTool = new Map();
|
|
14
|
-
const byId = new Map();
|
|
15
|
-
for (const ext of installed) {
|
|
16
|
-
byId.set(ext.id, ext);
|
|
17
|
-
for (const tool of ext.manifest.tools) {
|
|
18
|
-
byTool.set(tool.name, { extension: ext, tool });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return { byTool, byId };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function resolveTool(
|
|
25
|
-
reg: Registry,
|
|
26
|
-
toolName: string,
|
|
27
|
-
): { extension: InstalledExtension; tool: ExtensionManifest["tools"][number] } {
|
|
28
|
-
const hit = reg.byTool.get(toolName);
|
|
29
|
-
if (!hit) {
|
|
30
|
-
throw new OmtError("UNKNOWN_TOOL", `unknown tool '${toolName}'`);
|
|
31
|
-
}
|
|
32
|
-
return hit;
|
|
33
|
-
}
|
|
6
|
+
|
|
7
|
+
export interface Registry {
|
|
8
|
+
byTool: Map<string, { extension: InstalledExtension; tool: ExtensionManifest["tools"][number] }>;
|
|
9
|
+
byId: Map<string, InstalledExtension>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRegistry(installed: InstalledExtension[]): Registry {
|
|
13
|
+
const byTool = new Map();
|
|
14
|
+
const byId = new Map();
|
|
15
|
+
for (const ext of installed) {
|
|
16
|
+
byId.set(ext.id, ext);
|
|
17
|
+
for (const tool of ext.manifest.tools) {
|
|
18
|
+
byTool.set(tool.name, { extension: ext, tool });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { byTool, byId };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveTool(
|
|
25
|
+
reg: Registry,
|
|
26
|
+
toolName: string,
|
|
27
|
+
): { extension: InstalledExtension; tool: ExtensionManifest["tools"][number] } {
|
|
28
|
+
const hit = reg.byTool.get(toolName);
|
|
29
|
+
if (!hit) {
|
|
30
|
+
throw new OmtError("UNKNOWN_TOOL", `unknown tool '${toolName}'`);
|
|
31
|
+
}
|
|
32
|
+
return hit;
|
|
33
|
+
}
|
package/src/core/result.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export interface OmtOk {
|
|
2
|
-
ok: true;
|
|
3
|
-
tool: string;
|
|
4
|
-
data: unknown;
|
|
5
|
-
meta: Record<string, unknown>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface OmtErr {
|
|
9
|
-
ok: false;
|
|
10
|
-
tool: string;
|
|
11
|
-
error: { code: string; message: string };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type OmtResult = OmtOk | OmtErr;
|
|
1
|
+
export interface OmtOk {
|
|
2
|
+
ok: true;
|
|
3
|
+
tool: string;
|
|
4
|
+
data: unknown;
|
|
5
|
+
meta: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface OmtErr {
|
|
9
|
+
ok: false;
|
|
10
|
+
tool: string;
|
|
11
|
+
error: { code: string; message: string; details?: unknown };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type OmtResult = OmtOk | OmtErr;
|