@oh-my-tool/cli 0.2.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/LICENSE +21 -0
- package/README.md +13 -0
- package/assets/skills/oh-my-tool/SKILL.md +77 -0
- package/bin/ohmytool.cjs +15 -0
- package/bin/ohmytool.ts +5 -0
- package/package.json +34 -0
- package/src/cli/commands/describe.ts +24 -0
- package/src/cli/commands/extension.ts +24 -0
- package/src/cli/commands/index.ts +7 -0
- package/src/cli/commands/integrate.ts +64 -0
- package/src/cli/commands/run.ts +39 -0
- package/src/cli/commands/search.ts +16 -0
- package/src/cli/commands/secret.ts +72 -0
- package/src/cli/context.ts +44 -0
- package/src/cli/index.ts +296 -0
- package/src/cli/parseArgs.ts +44 -0
- package/src/config/config.ts +63 -0
- package/src/core/executor.ts +99 -0
- package/src/core/registry.ts +33 -0
- package/src/core/result.ts +14 -0
- package/src/core/schema.ts +2 -0
- package/src/extension/discovery.ts +62 -0
- package/src/extension/install.ts +24 -0
- package/src/extension/loader.ts +32 -0
- package/src/extension/manifest.ts +115 -0
- package/src/integration/adapters.ts +98 -0
- package/src/integration/index.ts +4 -0
- package/src/integration/manager.ts +375 -0
- package/src/integration/skill.ts +84 -0
- package/src/integration/types.ts +55 -0
- package/src/migration.ts +44 -0
- package/src/paths.ts +41 -0
- package/src/policy/policy.ts +139 -0
- package/src/runtime/errors.ts +8 -0
- package/src/runtime/executor.ts +66 -0
- package/src/runtime/provider-registry.ts +23 -0
- package/src/runtime/provider.ts +29 -0
- package/src/runtime/providers/native/discovery.ts +2 -0
- package/src/runtime/providers/native/install.ts +2 -0
- package/src/runtime/providers/native/loader.ts +1 -0
- package/src/runtime/providers/native/manifest.ts +6 -0
- package/src/runtime/providers/native/provider.ts +57 -0
- package/src/runtime/result.ts +12 -0
- package/src/runtime/runtime.ts +71 -0
- package/src/runtime/schema.ts +49 -0
- package/src/runtime/tool-registry.ts +54 -0
- package/src/search/search.ts +78 -0
- package/src/secrets/secrets.ts +45 -0
- package/src/version.ts +1 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { parseArgs } from "./parseArgs";
|
|
2
|
+
import { runSearch } from "./commands/search";
|
|
3
|
+
import { runDescribe } from "./commands/describe";
|
|
4
|
+
import { runTool } from "./commands/run";
|
|
5
|
+
import { runExtensionList, runExtensionInstall } from "./commands/extension";
|
|
6
|
+
import { runSecretList, runSecretSet } from "./commands/secret";
|
|
7
|
+
import {
|
|
8
|
+
defaultIntegrationManager,
|
|
9
|
+
runIntegrate,
|
|
10
|
+
type IntegrateAction,
|
|
11
|
+
} from "./commands/integrate";
|
|
12
|
+
import type { AgentDetection, AgentId, IntegrationResult, IntegrationStatus } from "../integration";
|
|
13
|
+
import { AGENT_IDS } from "../integration";
|
|
14
|
+
import { multiselect, isCancel } from "@clack/prompts";
|
|
15
|
+
import { VERSION } from "../version";
|
|
16
|
+
|
|
17
|
+
const HELP = `Oh My Tool - local and enterprise tools for agents
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
ohmytool search "<task>" search tools by intent
|
|
21
|
+
ohmytool describe <tool> inspect a tool and its input schema
|
|
22
|
+
ohmytool run <tool> [key=value ...] execute a tool
|
|
23
|
+
ohmytool run <tool> --stdin execute with JSON from stdin
|
|
24
|
+
ohmytool extension list list installed extensions
|
|
25
|
+
ohmytool extension install <path> install an extension from a local dir
|
|
26
|
+
ohmytool secret set <name> set a secret (interactive hidden prompt or stdin pipe)
|
|
27
|
+
ohmytool secret list list secret names (Windows only, values never shown)
|
|
28
|
+
ohmytool setup detect agents and install the OMT skill
|
|
29
|
+
ohmytool integrate [status|repair|uninstall]
|
|
30
|
+
manage agent skill integrations
|
|
31
|
+
ohmytool --version print version
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
ohmytool search "查询 mysql 设备数据"
|
|
35
|
+
ohmytool describe mysql.query
|
|
36
|
+
ohmytool run mysql.query connection=iot-test sql="SELECT id FROM device"
|
|
37
|
+
echo '{"connection":"iot-test","sql":"SELECT 1"}' | ohmytool run mysql.query --stdin
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const AGENT_IDS_SET = new Set<AgentId>(AGENT_IDS);
|
|
41
|
+
|
|
42
|
+
function readStdin(): Promise<string> {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let data = "";
|
|
45
|
+
process.stdin.setEncoding("utf8");
|
|
46
|
+
process.stdin.on("data", (chunk: string) => (data += chunk));
|
|
47
|
+
process.stdin.on("end", () => resolve(data.trim()));
|
|
48
|
+
process.stdin.on("error", reject);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 交互式隐藏输入(不回显、不进历史、不落盘),仅 TTY 下调用。 */
|
|
53
|
+
function readSecretHidden(prompt: string): Promise<string> {
|
|
54
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
55
|
+
process.stdout.write(prompt);
|
|
56
|
+
const stdin = process.stdin;
|
|
57
|
+
const prevRaw = stdin.isRaw;
|
|
58
|
+
stdin.setRawMode(true);
|
|
59
|
+
stdin.resume();
|
|
60
|
+
stdin.setEncoding("utf8");
|
|
61
|
+
let value = "";
|
|
62
|
+
const finish = () => {
|
|
63
|
+
stdin.removeListener("data", onData);
|
|
64
|
+
stdin.setRawMode(prevRaw);
|
|
65
|
+
stdin.pause();
|
|
66
|
+
};
|
|
67
|
+
const onData = (chunk: string) => {
|
|
68
|
+
for (const ch of chunk) {
|
|
69
|
+
if (ch === "\r" || ch === "\n") {
|
|
70
|
+
finish();
|
|
71
|
+
process.stdout.write("\n");
|
|
72
|
+
resolve(value);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (ch === "\x03") {
|
|
76
|
+
finish();
|
|
77
|
+
process.stdout.write("\n");
|
|
78
|
+
reject(new Error("aborted"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (ch === "\x7f" || ch === "\b") {
|
|
82
|
+
value = value.slice(0, -1);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
value += ch;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
stdin.on("data", onData);
|
|
89
|
+
return promise;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function print(v: unknown): void {
|
|
93
|
+
console.log(JSON.stringify(v, null, 2));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseAgentIds(raw?: string): AgentId[] | undefined {
|
|
97
|
+
if (!raw) return undefined;
|
|
98
|
+
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
99
|
+
for (const value of values) {
|
|
100
|
+
if (!AGENT_IDS_SET.has(value as AgentId)) throw new Error(`Unknown agent: ${value}`);
|
|
101
|
+
}
|
|
102
|
+
return [...new Set(values as AgentId[])];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printDetected(agents: AgentDetection[]): void {
|
|
106
|
+
console.log("Detected agents:\n");
|
|
107
|
+
for (const agent of agents) console.log(`✓ ${agent.variant ?? agent.displayName}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const STATUS_ICON: Record<IntegrationStatus, string> = {
|
|
111
|
+
current: "✓",
|
|
112
|
+
installed: "✓",
|
|
113
|
+
repaired: "✓",
|
|
114
|
+
uninstalled: "✓",
|
|
115
|
+
"update-available": "↻",
|
|
116
|
+
"not-installed": "○",
|
|
117
|
+
broken: "⚠",
|
|
118
|
+
conflict: "✗",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const STATUS_SEVERITY: Record<IntegrationStatus, number> = {
|
|
122
|
+
conflict: 0,
|
|
123
|
+
broken: 1,
|
|
124
|
+
"update-available": 2,
|
|
125
|
+
"not-installed": 3,
|
|
126
|
+
current: 4,
|
|
127
|
+
installed: 4,
|
|
128
|
+
repaired: 4,
|
|
129
|
+
uninstalled: 4,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function printIntegrationResults(results: IntegrationResult[]): void {
|
|
133
|
+
console.log("");
|
|
134
|
+
for (const item of results) {
|
|
135
|
+
const suffix = item.detail ? ` — ${item.detail}` : "";
|
|
136
|
+
console.log(`${STATUS_ICON[item.status]} ${item.displayName.padEnd(14)} ${item.status}${suffix}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function printStatus(results: IntegrationResult[]): void {
|
|
141
|
+
console.log("");
|
|
142
|
+
const ordered = [...results].sort((a, b) => STATUS_SEVERITY[a.status] - STATUS_SEVERITY[b.status]);
|
|
143
|
+
for (const item of ordered) {
|
|
144
|
+
console.log(
|
|
145
|
+
`${STATUS_ICON[item.status]} ${item.displayName.padEnd(14)} ${item.status.padEnd(19)} ${item.detail ?? item.target}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const counts: Record<string, number> = {};
|
|
149
|
+
for (const item of results) counts[item.status] = (counts[item.status] ?? 0) + 1;
|
|
150
|
+
const summary = Object.entries(counts).map(([status, n]) => `${n} ${status}`).join(" · ");
|
|
151
|
+
console.log(`\nSummary: ${summary}`);
|
|
152
|
+
if (results.some((item) => item.status === "broken")) {
|
|
153
|
+
console.log("Tip: run `omt integrate repair` to recreate broken links");
|
|
154
|
+
}
|
|
155
|
+
if (results.some((item) => item.status === "conflict")) {
|
|
156
|
+
console.log("Tip: conflict means OMT refuses to touch an unmanaged path; run `omt integrate --force` only if you accept replacing it");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function promptForAgents(agents: AgentDetection[]): Promise<AgentId[]> {
|
|
161
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
162
|
+
throw new Error("Interactive confirmation requires a TTY; pass --yes for unattended setup");
|
|
163
|
+
}
|
|
164
|
+
const selected = await multiselect({
|
|
165
|
+
message: "选择要集成的 Agent(空格切换,回车确认)",
|
|
166
|
+
options: agents.map((agent) => ({
|
|
167
|
+
value: agent.id,
|
|
168
|
+
label: agent.variant ?? agent.displayName,
|
|
169
|
+
hint: agent.target,
|
|
170
|
+
})),
|
|
171
|
+
initialValues: agents.map((agent) => agent.id),
|
|
172
|
+
required: false,
|
|
173
|
+
});
|
|
174
|
+
if (isCancel(selected) || !selected?.length) return [];
|
|
175
|
+
return selected as AgentId[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function main(argv: string[]): Promise<number> {
|
|
179
|
+
const parsed = parseArgs(argv);
|
|
180
|
+
const cmd = parsed.positional[0];
|
|
181
|
+
try {
|
|
182
|
+
switch (cmd) {
|
|
183
|
+
case "search": {
|
|
184
|
+
const q = parsed.positional.slice(1).join(" ");
|
|
185
|
+
print(await runSearch(q));
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
case "describe": {
|
|
189
|
+
print(await runDescribe(parsed.positional[1]));
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
case "run": {
|
|
193
|
+
const tool = parsed.positional[1];
|
|
194
|
+
const res = await runTool(tool, parsed.keyValues, parsed.flags.includes("stdin"));
|
|
195
|
+
print(res);
|
|
196
|
+
return res.ok ? 0 : 1;
|
|
197
|
+
}
|
|
198
|
+
case "secret": {
|
|
199
|
+
const sub = parsed.positional[1];
|
|
200
|
+
if (sub === "set") {
|
|
201
|
+
const name = parsed.positional[2];
|
|
202
|
+
if (!name) {
|
|
203
|
+
console.error("usage: ohmytool secret set <name> (交互输入或 stdin 管道)");
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
// TTY 下交互隐藏输入(不回显/不进历史),非 TTY 保持管道 stdin
|
|
207
|
+
const value = process.stdin.isTTY ? await readSecretHidden("password: ") : await readStdin();
|
|
208
|
+
print(await runSecretSet(name, value));
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (sub === "list") {
|
|
212
|
+
print(await runSecretList());
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
console.error("usage: ohmytool secret set <name> | secret list");
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
case "extension": {
|
|
219
|
+
const sub = parsed.positional[1];
|
|
220
|
+
if (sub === "list") {
|
|
221
|
+
print(await runExtensionList());
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (sub === "install") {
|
|
225
|
+
print(await runExtensionInstall(parsed.positional[2]));
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
console.error("usage: ohmytool extension list|install <path>");
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
case "setup":
|
|
232
|
+
case "integrate": {
|
|
233
|
+
const action = (cmd === "setup" ? "install" : parsed.positional[1] ?? "install") as IntegrateAction;
|
|
234
|
+
if (!["install", "status", "repair", "uninstall"].includes(action)) {
|
|
235
|
+
throw new Error(`Unknown integrate action: ${action}`);
|
|
236
|
+
}
|
|
237
|
+
const manager = defaultIntegrationManager();
|
|
238
|
+
const detected = await manager.detect();
|
|
239
|
+
if (!detected.length) throw new Error("No supported agents detected");
|
|
240
|
+
printDetected(detected);
|
|
241
|
+
let agents = parseAgentIds(parsed.options.agents);
|
|
242
|
+
const mutating = action !== "status" && !parsed.flags.includes("dry-run");
|
|
243
|
+
if (mutating && !parsed.flags.includes("yes") && !agents) {
|
|
244
|
+
agents = await promptForAgents(detected);
|
|
245
|
+
if (!agents.length) {
|
|
246
|
+
console.log("\nCancelled.");
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
} else if (mutating && !parsed.flags.includes("yes")) {
|
|
250
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
251
|
+
throw new Error("Confirmation required; pass --yes for unattended setup");
|
|
252
|
+
}
|
|
253
|
+
const selectedDetections = detected.filter((agent) => agents!.includes(agent.id));
|
|
254
|
+
agents = await promptForAgents(selectedDetections);
|
|
255
|
+
if (!agents.length) {
|
|
256
|
+
console.log("\nCancelled.");
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const integration = await runIntegrate(
|
|
261
|
+
{
|
|
262
|
+
action,
|
|
263
|
+
agents,
|
|
264
|
+
force: parsed.flags.includes("force"),
|
|
265
|
+
dryRun: parsed.flags.includes("dry-run"),
|
|
266
|
+
},
|
|
267
|
+
manager,
|
|
268
|
+
);
|
|
269
|
+
if (integration.dryRun) {
|
|
270
|
+
console.log(`\nDry run: would ${action} ${integration.selected.join(", ")}`);
|
|
271
|
+
} else if (action === "status") {
|
|
272
|
+
printStatus(integration.results);
|
|
273
|
+
} else {
|
|
274
|
+
printIntegrationResults(integration.results);
|
|
275
|
+
}
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
case "-v": {
|
|
279
|
+
print({ name: "ohmytool", version: VERSION });
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
default:
|
|
283
|
+
if (parsed.flags.includes("version")) {
|
|
284
|
+
print({ name: "ohmytool", version: VERSION });
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
console.log(HELP);
|
|
288
|
+
return cmd ? 1 : 0;
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
@@ -0,0 +1,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 function parseArgs(argv: string[]): ParsedArgs {
|
|
9
|
+
const positional: string[] = [];
|
|
10
|
+
const keyValues: Record<string, string> = {};
|
|
11
|
+
const flags: string[] = [];
|
|
12
|
+
const options: Record<string, string> = {};
|
|
13
|
+
for (const arg of argv) {
|
|
14
|
+
if (arg.startsWith("--")) {
|
|
15
|
+
const option = arg.slice(2);
|
|
16
|
+
const eq = option.indexOf("=");
|
|
17
|
+
if (eq > 0) {
|
|
18
|
+
options[option.slice(0, eq)] = option.slice(eq + 1);
|
|
19
|
+
} else {
|
|
20
|
+
flags.push(option);
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
const eq = arg.indexOf("=");
|
|
24
|
+
if (eq > 0) {
|
|
25
|
+
keyValues[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
26
|
+
} else {
|
|
27
|
+
positional.push(arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { positional, keyValues, flags, options };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function coerceInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
35
|
+
const out: Record<string, unknown> = {};
|
|
36
|
+
for (const [k, v] of Object.entries(input)) {
|
|
37
|
+
if (typeof v === "string" && /^-?\d+$/.test(v)) {
|
|
38
|
+
out[k] = Number(v);
|
|
39
|
+
} else {
|
|
40
|
+
out[k] = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ConnectionConfig {
|
|
5
|
+
environment: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
database: string;
|
|
9
|
+
username: string;
|
|
10
|
+
secret: string;
|
|
11
|
+
tls: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Config {
|
|
15
|
+
extensions: Record<string, { connections: Record<string, ConnectionConfig> }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadConfig(homeDir: string): Config {
|
|
19
|
+
const path = join(homeDir, "config.toml");
|
|
20
|
+
if (!existsSync(path)) {
|
|
21
|
+
return { extensions: {} };
|
|
22
|
+
}
|
|
23
|
+
const raw = readFileSync(path, "utf8");
|
|
24
|
+
const parsed = Bun.TOML.parse(raw) as Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
const extensions: Config["extensions"] = {};
|
|
27
|
+
const extSection = (parsed as Record<string, any>)["extensions"];
|
|
28
|
+
if (extSection && typeof extSection === "object") {
|
|
29
|
+
for (const [extId, extVal] of Object.entries(extSection)) {
|
|
30
|
+
const connections: Record<string, ConnectionConfig> = {};
|
|
31
|
+
const connSection = (extVal as any)?.["connections"];
|
|
32
|
+
if (connSection && typeof connSection === "object") {
|
|
33
|
+
for (const [name, rawConn] of Object.entries(connSection)) {
|
|
34
|
+
const rc = rawConn as Record<string, any>;
|
|
35
|
+
connections[name] = {
|
|
36
|
+
environment: String(rc.environment ?? ""),
|
|
37
|
+
host: String(rc.host ?? ""),
|
|
38
|
+
port: Number(rc.port ?? 3306),
|
|
39
|
+
database: String(rc.database ?? ""),
|
|
40
|
+
username: String(rc.username ?? ""),
|
|
41
|
+
secret: String(rc.secret ?? ""),
|
|
42
|
+
tls: Boolean(rc.tls ?? false),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
extensions[extId] = { connections };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { extensions };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getConnectionConfig(
|
|
53
|
+
cfg: Config,
|
|
54
|
+
extensionId: string,
|
|
55
|
+
connection: string,
|
|
56
|
+
): ConnectionConfig | undefined {
|
|
57
|
+
return cfg.extensions[extensionId]?.connections[connection];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listConnections(cfg: Config, extensionId: string): string[] {
|
|
61
|
+
const conns = cfg.extensions[extensionId]?.connections ?? {};
|
|
62
|
+
return Object.keys(conns);
|
|
63
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
if (e instanceof OmtError) {
|
|
83
|
+
return { ok: false, tool: toolName, error: { code: e.code, message: e.message } };
|
|
84
|
+
}
|
|
85
|
+
if (e && typeof e === "object" && "code" in e && typeof (e as { code?: unknown }).code === "string") {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
tool: toolName,
|
|
89
|
+
error: { code: (e as { code: string }).code, message: e instanceof Error ? e.message : String(e) },
|
|
90
|
+
};
|
|
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
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionManifest } from "@oh-my-tool/sdk";
|
|
2
|
+
import type { InstalledExtension } from "../extension/discovery";
|
|
3
|
+
|
|
4
|
+
import { RuntimeError as OmtError } from "../runtime/errors";
|
|
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
|
+
}
|
|
@@ -0,0 +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;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionManifest } from "@oh-my-tool/sdk";
|
|
4
|
+
import { parseManifest, validateManifest, checkSdkCompatibility } from "./manifest";
|
|
5
|
+
|
|
6
|
+
export interface InstalledExtension {
|
|
7
|
+
id: string;
|
|
8
|
+
version: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
manifest: ExtensionManifest;
|
|
11
|
+
entry: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EXTENSIONS_DIR = "extensions";
|
|
15
|
+
|
|
16
|
+
function readEntry(dir: string): string {
|
|
17
|
+
const pkgPath = join(dir, "package.json");
|
|
18
|
+
if (existsSync(pkgPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, any>;
|
|
21
|
+
const entry = pkg.omt?.entry;
|
|
22
|
+
if (typeof entry === "string") {
|
|
23
|
+
return join(dir, entry.replace(/^\.\//, ""));
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore malformed package.json, fall through
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return join(dir, "src", "index.ts");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function discoverExtensions(home: string): InstalledExtension[] {
|
|
33
|
+
const root = join(home, EXTENSIONS_DIR);
|
|
34
|
+
if (!existsSync(root)) return [];
|
|
35
|
+
|
|
36
|
+
const out: InstalledExtension[] = [];
|
|
37
|
+
for (const id of readdirSync(root)) {
|
|
38
|
+
const idDir = join(root, id);
|
|
39
|
+
if (!statSync(idDir).isDirectory()) continue;
|
|
40
|
+
for (const version of readdirSync(idDir)) {
|
|
41
|
+
const versionDir = join(idDir, version);
|
|
42
|
+
if (!statSync(versionDir).isDirectory()) continue;
|
|
43
|
+
const manifestPath = join(versionDir, "omt.manifest.json");
|
|
44
|
+
if (!existsSync(manifestPath)) continue;
|
|
45
|
+
try {
|
|
46
|
+
const manifest = parseManifest(readFileSync(manifestPath, "utf8"));
|
|
47
|
+
validateManifest(manifest);
|
|
48
|
+
checkSdkCompatibility(manifest.sdkVersion);
|
|
49
|
+
out.push({
|
|
50
|
+
id,
|
|
51
|
+
version,
|
|
52
|
+
dir: versionDir,
|
|
53
|
+
manifest,
|
|
54
|
+
entry: readEntry(versionDir),
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
// skip invalid or incompatible manifests
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cp, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseManifest, validateManifest, checkSdkCompatibility } from "./manifest";
|
|
4
|
+
|
|
5
|
+
export interface InstalledRef {
|
|
6
|
+
id: string;
|
|
7
|
+
version: string;
|
|
8
|
+
target: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function installLocalExtension(home: string, srcDir: string): Promise<InstalledRef> {
|
|
12
|
+
const manifest = parseManifest(await readFile(join(srcDir, "omt.manifest.json"), "utf8"));
|
|
13
|
+
validateManifest(manifest);
|
|
14
|
+
checkSdkCompatibility(manifest.sdkVersion);
|
|
15
|
+
const target = join(home, "extensions", manifest.id, manifest.version);
|
|
16
|
+
await mkdir(target, { recursive: true });
|
|
17
|
+
// 显式 force:true:Bun 的 fs.cp 在带 filter 时默认覆盖失效(重装不更新旧文件)
|
|
18
|
+
await cp(srcDir, target, {
|
|
19
|
+
recursive: true,
|
|
20
|
+
force: true,
|
|
21
|
+
filter: (s: string) => !s.includes("node_modules"),
|
|
22
|
+
});
|
|
23
|
+
return { id: manifest.id, version: manifest.version, target };
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import type { ExtensionDefinition } from "@oh-my-tool/sdk";
|
|
3
|
+
import { OmtError } from "../core/registry";
|
|
4
|
+
import type { InstalledExtension } from "./discovery";
|
|
5
|
+
import { validateHandlers } from "./manifest";
|
|
6
|
+
|
|
7
|
+
export async function loadExtension(
|
|
8
|
+
installed: InstalledExtension,
|
|
9
|
+
): Promise<ExtensionDefinition> {
|
|
10
|
+
let mod: unknown;
|
|
11
|
+
try {
|
|
12
|
+
mod = await import(pathToFileURL(installed.entry).href);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
throw new OmtError(
|
|
15
|
+
"LOAD_FAILED",
|
|
16
|
+
`failed to load extension '${installed.id}': ${e instanceof Error ? e.message : String(e)}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const def = (mod as any)?.default ?? mod;
|
|
21
|
+
if (!def || typeof def.handlers !== "object" || def.handlers === null) {
|
|
22
|
+
throw new OmtError("LOAD_FAILED", `extension '${installed.id}' has no handlers`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const handlerNames = Object.keys(def.handlers);
|
|
26
|
+
try {
|
|
27
|
+
validateHandlers(installed.manifest, handlerNames);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
throw new OmtError("LOAD_FAILED", (e as Error).message);
|
|
30
|
+
}
|
|
31
|
+
return def as ExtensionDefinition;
|
|
32
|
+
}
|