@oh-my-tool/cli 0.3.2 → 0.3.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-tool/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Oh My Tool CLI - local and enterprise tools for agents",
6
6
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@clack/prompts": "^1.7.0",
31
31
  "@modelcontextprotocol/client": "2.0.0",
32
- "@oh-my-tool/sdk": "0.3.2",
32
+ "@oh-my-tool/sdk": "0.3.4",
33
33
  "open": "11.0.0"
34
34
  },
35
35
  "engines": {
@@ -1,18 +1,15 @@
1
- import { loadConfig } from "../../config/config";
1
+ import { loadConfig, sanitizeExtensionConnections, validateConfiguredConnections } from "../../config/config";
2
2
  import { createPaths } from "../../paths";
3
3
  import { prepareHome } from "../../migration";
4
4
  import { withRuntime } from "../context";
5
+ import { discoverExtensions } from "../../extension/discovery";
5
6
 
6
7
  export interface ConnectionSummary {
7
8
  extension: string;
8
9
  name: string;
9
- environment: string;
10
- host: string;
11
- port: number;
12
- database: string;
13
- username: string;
14
- tls: boolean;
15
- secretConfigured: boolean;
10
+ environment?: string;
11
+ settings: Record<string, unknown>;
12
+ secretsConfigured: Record<string, boolean>;
16
13
  }
17
14
 
18
15
  export interface ConnectionListResult {
@@ -43,20 +40,15 @@ async function configuredConnections(): Promise<ConnectionListResult> {
43
40
  const paths = createPaths();
44
41
  await prepareHome(paths);
45
42
  const config = loadConfig(paths.home);
46
- const connections = Object.entries(config.extensions)
43
+ const sanitized = sanitizeExtensionConnections(config);
44
+ const connections = Object.entries(sanitized)
47
45
  .sort(([left], [right]) => left.localeCompare(right))
48
- .flatMap(([extension, value]) => Object.entries(value.connections)
46
+ .flatMap(([extension, value]) => Object.entries(value)
49
47
  .sort(([left], [right]) => left.localeCompare(right))
50
48
  .map(([name, connection]) => ({
51
49
  extension,
52
50
  name,
53
- environment: connection.environment,
54
- host: connection.host,
55
- port: connection.port,
56
- database: connection.database,
57
- username: connection.username,
58
- tls: connection.tls,
59
- secretConfigured: connection.secret.length > 0,
51
+ ...connection,
60
52
  })));
61
53
  return { connections, count: connections.length };
62
54
  }
@@ -69,6 +61,7 @@ export async function runConfigCheck(): Promise<ConfigCheckResult> {
69
61
  const paths = createPaths();
70
62
  await prepareHome(paths);
71
63
  const config = loadConfig(paths.home);
64
+ validateConfiguredConnections(config, discoverExtensions(paths.home));
72
65
  return {
73
66
  valid: true,
74
67
  connectionCount: Object.values(config.extensions).reduce((count, extension) => count + Object.keys(extension.connections).length, 0),
@@ -78,17 +71,32 @@ export async function runConfigCheck(): Promise<ConfigCheckResult> {
78
71
 
79
72
  export async function runConnectionCheck(): Promise<ConnectionCheckResult> {
80
73
  const list = await configuredConnections();
74
+ const paths = createPaths();
75
+ const checkTools = new Map(discoverExtensions(paths.home).map((extension) => [extension.id, extension.manifest.connectionCheckTool]));
81
76
  return withRuntime(async (runtime) => {
82
- const checks: ConnectionCheck[] = [];
83
- for (const connection of list.connections) {
77
+ const checks: ConnectionCheck[] = await boundedMap(list.connections, 4, async (connection) => {
78
+ const toolId = checkTools.get(connection.extension);
79
+ if (!toolId) return { extension: connection.extension, name: connection.name, status: "unsupported", code: "CHECK_UNSUPPORTED" };
84
80
  const started = Date.now();
85
- const result = await runtime.run(`${connection.extension}.ping`, { connection: connection.name });
86
- checks.push(result.ok
81
+ const result = await runtime.run(toolId, { connection: connection.name });
82
+ return result.ok
87
83
  ? { extension: connection.extension, name: connection.name, status: "ok", durationMs: Date.now() - started }
88
- : result.error?.code === "TOOL_NOT_FOUND"
89
- ? { extension: connection.extension, name: connection.name, status: "unsupported", code: "CHECK_UNSUPPORTED" }
90
- : { extension: connection.extension, name: connection.name, status: "error", code: result.error?.code ?? "CHECK_FAILED", durationMs: Date.now() - started });
91
- }
84
+ : { extension: connection.extension, name: connection.name, status: "error", code: result.error.code ?? "CHECK_FAILED", durationMs: Date.now() - started };
85
+ });
92
86
  return { checks, count: checks.length };
93
87
  }, { includeMcp: false });
94
88
  }
89
+
90
+ async function boundedMap<T, R>(values: readonly T[], concurrency: number, worker: (value: T) => Promise<R>): Promise<R[]> {
91
+ const results = new Array<R>(values.length);
92
+ let next = 0;
93
+ async function consume(): Promise<void> {
94
+ while (true) {
95
+ const index = next++;
96
+ if (index >= values.length) return;
97
+ results[index] = await worker(values[index]);
98
+ }
99
+ }
100
+ await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => consume()));
101
+ return results;
102
+ }
@@ -1,25 +1,19 @@
1
1
  import { coerceInput } from "../parseArgs";
2
2
  import { withRuntime } from "../context";
3
- import type { OmtResult } from "../../core/result";
3
+ import type { ExecutionResult } from "../../runtime/result";
4
4
 
5
5
  export async function runTool(
6
6
  toolName: string,
7
7
  keyValues: Record<string, string>,
8
8
  useStdin: boolean,
9
- ): Promise<OmtResult> {
9
+ ): Promise<ExecutionResult> {
10
10
  let input: Record<string, unknown>;
11
11
  if (useStdin) {
12
12
  input = await readStdinJson();
13
13
  } else {
14
14
  input = coerceInput(keyValues);
15
15
  }
16
- return withRuntime(async (runtime) => {
17
- const result = await runtime.run(toolName, input);
18
- if (result.ok) {
19
- return { ok: true, tool: toolName, data: result.output, meta: result.meta ?? {} };
20
- }
21
- return { ok: false, tool: toolName, error: result.error ?? { code: "EXECUTION_FAILED", message: "execution failed" } };
22
- }, { targetTool: toolName });
16
+ return withRuntime((runtime) => runtime.run(toolName, input), { targetTool: toolName });
23
17
  }
24
18
 
25
19
  function readStdinJson(): Promise<Record<string, unknown>> {
@@ -1,8 +1,9 @@
1
1
  import { withRuntime } from "../context";
2
+ import type { ToolSearchOptions } from "../../runtime/provider";
2
3
 
3
- export async function runSearch(query: string): Promise<{ tools: Array<Record<string, unknown>> }> {
4
+ export async function runSearch(query: string, options: ToolSearchOptions = {}): Promise<{ tools: Array<Record<string, unknown>>; meta: { unavailableProviders: Array<Record<string, unknown>> } }> {
4
5
  return withRuntime(async (runtime) => {
5
- const descriptors = await runtime.search(query);
6
+ const descriptors = await runtime.search(query, options);
6
7
  return {
7
8
  tools: descriptors.map((descriptor) => ({
8
9
  name: descriptor.id,
@@ -11,6 +12,11 @@ export async function runSearch(query: string): Promise<{ tools: Array<Record<st
11
12
  risk: descriptor.risk,
12
13
  provider: descriptor.provider,
13
14
  })),
15
+ meta: {
16
+ unavailableProviders: runtime.providerStatuses()
17
+ .filter((status) => status.status === "unavailable")
18
+ .map(({ id, kind, status, code, message }) => ({ id, kind, status, ...(code === undefined ? {} : { code }), ...(message === undefined ? {} : { message }) })),
19
+ },
14
20
  };
15
21
  });
16
22
  }
@@ -1,11 +1,10 @@
1
1
  import { createPaths } from "../paths";
2
2
  import { prepareHome } from "../migration";
3
- import { loadConfig, getConnectionConfig, sanitizeExtensionConnections, type McpEnabledServerConfig } from "../config/config";
3
+ import { loadConfig, getConnectionConfig, sanitizeExtensionConnections, validateConfiguredConnections, type McpEnabledServerConfig } from "../config/config";
4
4
  import { SecretsManager } from "../secrets/secrets";
5
5
  import { applyLimits, validateConnectionInput } from "../policy/policy";
6
6
  import { NativeExtensionProvider } from "../runtime/providers/native/provider";
7
7
  import { McpProvider } from "../runtime/providers/mcp/provider";
8
- import { discoverExtensions } from "../extension/discovery";
9
8
  import { createToolRuntime } from "../runtime/runtime";
10
9
  import type { ToolDescriptor } from "../runtime/provider";
11
10
 
@@ -24,13 +23,19 @@ export async function createRuntime(options: RuntimeOptions = {}) {
24
23
  const config = loadConfig(paths.home);
25
24
  const secrets = new SecretsManager();
26
25
  const extensionConnections = sanitizeExtensionConnections(config);
27
- const nativeTarget = options.targetTool !== undefined && discoverExtensions(paths.home)
28
- .some((extension) => extension.manifest.tools.some((tool) => tool.name === options.targetTool));
26
+ const nativeProvider = new NativeExtensionProvider(paths);
27
+ const nativeTargetExtension = options.targetTool === undefined
28
+ ? undefined
29
+ : nativeProvider.extensionForTool(options.targetTool);
30
+ if (nativeTargetExtension !== undefined) {
31
+ validateConfiguredConnections(config, nativeProvider.installedExtensions(), nativeTargetExtension);
32
+ }
33
+ const nativeTarget = nativeTargetExtension !== undefined;
29
34
  const mcpProviders = options.includeMcp === false || nativeTarget ? [] : Object.entries(config.mcp.servers)
30
35
  .sort(([a], [b]) => a.localeCompare(b))
31
36
  .filter((entry): entry is [string, McpEnabledServerConfig] => entry[1].enabled)
32
37
  .map(([serverId, server]) => new McpProvider({ serverId, config: server, secrets }));
33
- const providers = [new NativeExtensionProvider(paths), ...mcpProviders];
38
+ const providers = [nativeProvider, ...mcpProviders];
34
39
  return createToolRuntime({
35
40
  providers,
36
41
  policy: {
package/src/cli/index.ts CHANGED
@@ -121,6 +121,23 @@ function parseAgentIds(raw?: string): AgentId[] | undefined {
121
121
  return [...new Set(values as AgentId[])];
122
122
  }
123
123
 
124
+ function parseSearchOptions(options: Record<string, string>): { limit?: number; provider?: string; source?: string; risk?: "read" | "write" | "admin" } {
125
+ const limit = options.limit === undefined ? undefined : Number(options.limit);
126
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
127
+ throw new RuntimeError("INVALID_ARGUMENT", "search --limit must be a positive integer");
128
+ }
129
+ const risk = options.risk;
130
+ if (risk !== undefined && risk !== "read" && risk !== "write" && risk !== "admin") {
131
+ throw new RuntimeError("INVALID_ARGUMENT", `unsupported search risk '${risk}'`);
132
+ }
133
+ return {
134
+ ...(limit === undefined ? {} : { limit }),
135
+ ...(options.provider === undefined ? {} : { provider: options.provider }),
136
+ ...(options.source === undefined ? {} : { source: options.source }),
137
+ ...(risk === undefined ? {} : { risk }),
138
+ };
139
+ }
140
+
124
141
  function printDetected(agents: AgentDetection[]): void {
125
142
  console.log("Detected agents:\n");
126
143
  for (const agent of agents) console.log(`✓ ${agent.variant ?? agent.displayName}`);
@@ -169,10 +186,10 @@ function printStatus(results: IntegrationResult[]): void {
169
186
  const summary = Object.entries(counts).map(([status, n]) => `${n} ${status}`).join(" · ");
170
187
  console.log(`\nSummary: ${summary}`);
171
188
  if (results.some((item) => item.status === "broken")) {
172
- console.log("Tip: run `omt integrate repair` to recreate broken links");
189
+ console.log("Tip: run `ohmytool integrate repair` to recreate broken links");
173
190
  }
174
191
  if (results.some((item) => item.status === "conflict")) {
175
- console.log("Tip: conflict means OMT refuses to touch an unmanaged path; run `omt integrate --force` only if you accept replacing it");
192
+ console.log("Tip: conflict means OMT refuses to touch an unmanaged path; run `ohmytool integrate --force` only if you accept replacing it");
176
193
  }
177
194
  }
178
195
 
@@ -207,7 +224,7 @@ export async function main(argv: string[], dependencies: CliDependencies = defau
207
224
  const parsed = parseArgs(argv);
208
225
  const cmd = parsed.positional[0];
209
226
  const allowedFlags = new Set(["help", "version", "json", "stdin", "yes", "dry-run", "force"]);
210
- const allowedOptions = new Set(["format", "json", "agents"]);
227
+ const allowedOptions = new Set(["format", "json", "agents", "limit", "provider", "source", "risk"]);
211
228
  const unknownFlag = parsed.flags.find((flag) => !allowedFlags.has(flag));
212
229
  const unknownOption = Object.keys(parsed.options).find((option) => !allowedOptions.has(option));
213
230
  if (unknownFlag) throw new RuntimeError("INVALID_ARGUMENT", `unknown option '--${unknownFlag}'`);
@@ -215,7 +232,7 @@ export async function main(argv: string[], dependencies: CliDependencies = defau
215
232
  switch (cmd) {
216
233
  case "search": {
217
234
  const q = parsed.positional.slice(1).join(" ");
218
- print(await runSearch(q));
235
+ print(await runSearch(q, parseSearchOptions(parsed.options)));
219
236
  return 0;
220
237
  }
221
238
  case "describe": {
@@ -235,7 +252,7 @@ export async function main(argv: string[], dependencies: CliDependencies = defau
235
252
  return 1;
236
253
  }
237
254
  const result = action === "list" ? await runConnectionList() : await runConnectionCheck();
238
- const wrapped = { ok: true as const, tool: `connection.${action}`, data: result, meta: {} };
255
+ const wrapped = { ok: true as const, toolId: `connection.${action}`, output: result, meta: {} };
239
256
  console.log(formatOutput(wrapped, outputFormat(parsed)));
240
257
  return 0;
241
258
  }
@@ -245,7 +262,7 @@ export async function main(argv: string[], dependencies: CliDependencies = defau
245
262
  return 1;
246
263
  }
247
264
  const result = await runConfigCheck();
248
- const wrapped = { ok: true as const, tool: "config.check", data: result, meta: {} };
265
+ const wrapped = { ok: true as const, toolId: "config.check", output: result, meta: {} };
249
266
  console.log(formatOutput(wrapped, outputFormat(parsed)));
250
267
  return 0;
251
268
  }
package/src/cli/output.ts CHANGED
@@ -1,24 +1,24 @@
1
- import type { OmtResult } from "../core/result";
1
+ import type { ExecutionResult } from "../runtime/result";
2
2
 
3
3
  export type OutputFormat = "text" | "json" | "table" | "csv";
4
4
 
5
- export function formatAiResult(result: OmtResult): string {
6
- const lines = [`status: ${result.ok ? "ok" : "error"}`, `tool: ${result.tool}`];
5
+ export function formatAiResult(result: ExecutionResult): string {
6
+ const lines = [`status: ${result.ok ? "ok" : "error"}`, `tool: ${result.toolId}`];
7
7
  if (!result.ok) {
8
8
  lines.push("error:");
9
9
  appendObject(lines, result.error, 2);
10
10
  return lines.join("\n");
11
11
  }
12
12
 
13
- if (isRowsResult(result.data)) {
13
+ if (isRowsResult(result.output)) {
14
14
  lines.push("rows:");
15
- for (const row of result.data.rows) lines.push(formatRow(row));
16
- lines.push(`row_count: ${result.data.rows.length}`);
17
- if (result.data.truncated === true) lines.push("truncated: true");
15
+ for (const row of result.output.rows) lines.push(formatRow(row));
16
+ lines.push(`row_count: ${result.output.rows.length}`);
17
+ if (result.output.truncated === true) lines.push("truncated: true");
18
18
  appendMeta(lines, result.meta, new Set(["returnedRows"]), false);
19
19
  } else {
20
20
  lines.push("data:");
21
- appendValue(lines, result.data, 2);
21
+ appendValue(lines, result.output, 2);
22
22
  appendMeta(lines, result.meta);
23
23
  }
24
24
  return lines.join("\n");
@@ -36,17 +36,17 @@ export function formatJson(value: unknown): string {
36
36
  }, 2) ?? "null";
37
37
  }
38
38
 
39
- export function formatOutput(result: OmtResult, format: OutputFormat): string {
39
+ export function formatOutput(result: ExecutionResult, format: OutputFormat): string {
40
40
  if (format === "json") return formatJson(result);
41
41
  if (format === "csv" || format === "table") return formatDelimited(result, format === "csv" ? "," : "\t");
42
42
  return formatAiResult(result);
43
43
  }
44
44
 
45
- function formatDelimited(result: OmtResult, delimiter: string): string {
45
+ function formatDelimited(result: ExecutionResult, delimiter: string): string {
46
46
  if (!result.ok) return formatAiResult(result);
47
- const rows = isRowsResult(result.data)
48
- ? { columns: result.data.columns, rows: result.data.rows }
49
- : findObjectArray(result.data);
47
+ const rows = isRowsResult(result.output)
48
+ ? { columns: result.output.columns, rows: result.output.rows }
49
+ : findObjectArray(result.output);
50
50
  if (!rows) return formatAiResult(result);
51
51
  return [rows.columns, ...rows.rows].map((row) => row.map((value) => quoteDelimited(value, delimiter)).join(delimiter)).join("\n");
52
52
  }
@@ -1,15 +1,19 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { RuntimeError } from "../runtime/errors";
4
+ import { validateInput, type Schema } from "../runtime/schema";
5
+ import type { InstalledExtension } from "../extension/discovery";
4
6
 
5
7
  export interface ConnectionConfig {
6
- environment: string;
7
- host: string;
8
- port: number;
9
- database: string;
10
- username: string;
11
- secret: string;
12
- tls: boolean;
8
+ environment?: string;
9
+ settings: Record<string, unknown>;
10
+ secrets: Record<string, string>;
11
+ }
12
+
13
+ export interface SanitizedConnectionConfig {
14
+ environment?: string;
15
+ settings: Record<string, unknown>;
16
+ secretsConfigured: Record<string, boolean>;
13
17
  }
14
18
 
15
19
  export interface McpCommonServerConfig { readonly enabled: true; readonly namespace: string; }
@@ -44,32 +48,36 @@ function parseConnection(extensionId: string, name: string, value: unknown): Con
44
48
  const path = `extensions.${extensionId}.connections.${name}`;
45
49
  if (value === null || typeof value !== "object" || Array.isArray(value)) invalidConnection(path, "must be a table");
46
50
  const raw = value as Record<string, unknown>;
47
- const stringField = (key: string, fallback = ""): string => {
48
- const entry = raw[key];
49
- if (entry === undefined) return fallback;
50
- if (typeof entry !== "string") invalidConnection(`${path}.${key}`, "must be a string");
51
- return entry;
52
- };
53
- const host = stringField("host");
54
- if (host.trim().length === 0) invalidConnection(`${path}.host`, "must be a non-empty string");
55
- const defaultPort = extensionId === "redis" ? 6379 : 3306;
56
- const port = raw.port === undefined ? defaultPort : raw.port;
57
- if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) {
58
- invalidConnection(`${path}.port`, "must be an integer from 1 through 65535");
59
- }
60
- const tls = raw.tls === undefined ? false : raw.tls;
61
- if (typeof tls !== "boolean") invalidConnection(`${path}.tls`, "must be a boolean");
51
+ const allowed = new Set(["environment", "settings", "secrets"]);
52
+ for (const key of Object.keys(raw)) if (!allowed.has(key)) invalidConnection(`${path}.${key}`, "unknown connection field");
53
+ const environment = raw.environment;
54
+ if (environment !== undefined && typeof environment !== "string") invalidConnection(`${path}.environment`, "must be a string");
55
+ const settings = parseSettingsMap(raw.settings, `${path}.settings`);
56
+ const secrets = parseSecretMap(raw.secrets, `${path}.secrets`);
62
57
  return {
63
- environment: stringField("environment"),
64
- host,
65
- port,
66
- database: stringField("database"),
67
- username: stringField("username"),
68
- secret: stringField("secret"),
69
- tls,
58
+ ...(environment === undefined ? {} : { environment }),
59
+ settings,
60
+ secrets,
70
61
  };
71
62
  }
72
63
 
64
+ function parseSettingsMap(value: unknown, path: string): Record<string, unknown> {
65
+ if (value === undefined) return {};
66
+ if (value === null || typeof value !== "object" || Array.isArray(value)) invalidConnection(path, "must be a table");
67
+ return { ...(value as Record<string, unknown>) };
68
+ }
69
+
70
+ function parseSecretMap(value: unknown, path: string): Record<string, string> {
71
+ if (value === undefined) return {};
72
+ if (value === null || typeof value !== "object" || Array.isArray(value)) invalidConnection(path, "must be a table");
73
+ const result: Record<string, string> = {};
74
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
75
+ if (typeof entry !== "string" || entry.length === 0) invalidConnection(`${path}.${key}`, "must be a non-empty string");
76
+ result[key] = entry;
77
+ }
78
+ return result;
79
+ }
80
+
73
81
  function parseStringMap(value: unknown, path: string): Record<string, string> {
74
82
  if (value === undefined) return {};
75
83
  if (value === null || typeof value !== "object" || Array.isArray(value)) invalid(path, "must be a table");
@@ -191,17 +199,34 @@ export function loadConfig(homeDir: string): Config {
191
199
  export function getConnectionConfig(cfg: Config, extensionId: string, connection: string): ConnectionConfig | undefined { return cfg.extensions[extensionId]?.connections[connection]; }
192
200
  export function listConnections(cfg: Config, extensionId: string): string[] { return Object.keys(cfg.extensions[extensionId]?.connections ?? {}); }
193
201
 
194
- export function sanitizeExtensionConnections(cfg: Config): Record<string, Record<string, Omit<ConnectionConfig, "secret"> & { secretConfigured: boolean }> > {
202
+ export function sanitizeExtensionConnections(cfg: Config): Record<string, Record<string, SanitizedConnectionConfig>> {
195
203
  return Object.fromEntries(Object.entries(cfg.extensions).map(([extensionId, extension]) => [
196
204
  extensionId,
197
205
  Object.fromEntries(Object.entries(extension.connections).map(([name, connection]) => [name, {
198
- environment: connection.environment,
199
- host: connection.host,
200
- port: connection.port,
201
- database: connection.database,
202
- username: connection.username,
203
- tls: connection.tls,
204
- secretConfigured: connection.secret.length > 0,
206
+ ...(connection.environment === undefined ? {} : { environment: connection.environment }),
207
+ settings: { ...connection.settings },
208
+ secretsConfigured: Object.fromEntries(Object.keys(connection.secrets).map((key) => [key, true])),
205
209
  }])),
206
210
  ]));
207
211
  }
212
+
213
+ export function validateConfiguredConnections(
214
+ cfg: Config,
215
+ installedExtensions: readonly InstalledExtension[],
216
+ targetExtensionId?: string,
217
+ ): void {
218
+ const manifests = new Map(installedExtensions.map((extension) => [extension.id, extension.manifest]));
219
+ for (const [extensionId, extension] of Object.entries(cfg.extensions)) {
220
+ if (targetExtensionId !== undefined && targetExtensionId !== extensionId) continue;
221
+ const schema = manifests.get(extensionId)?.connectionSchema;
222
+ if (schema === undefined) continue;
223
+ for (const [name, connection] of Object.entries(extension.connections)) {
224
+ try {
225
+ validateInput(schema as Schema, connection.settings, { applyDefaults: false });
226
+ } catch (error) {
227
+ const message = error instanceof Error ? error.message : String(error);
228
+ invalidConnection(`extensions.${extensionId}.connections.${name}.settings`, message);
229
+ }
230
+ }
231
+ }
232
+ }
@@ -1,6 +1,6 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  import type { ExtensionDefinition } from "@oh-my-tool/sdk";
3
- import { OmtError } from "../core/registry";
3
+ import { RuntimeError as OmtError } from "../runtime/errors";
4
4
  import type { InstalledExtension } from "./discovery";
5
5
  import { validateHandlers } from "./manifest";
6
6
 
@@ -35,6 +35,11 @@ export function validateManifest(manifest: ExtensionManifest): void {
35
35
  if (!manifest.sdkVersion || typeof manifest.sdkVersion !== "string") {
36
36
  throw new ManifestError("manifest must contain a string 'sdkVersion'");
37
37
  }
38
+ if (manifest.connectionSchema !== undefined && (
39
+ manifest.connectionSchema === null || typeof manifest.connectionSchema !== "object" || Array.isArray(manifest.connectionSchema)
40
+ )) {
41
+ throw new ManifestError("manifest 'connectionSchema' must be an object");
42
+ }
38
43
 
39
44
  const seen = new Set<string>();
40
45
  for (const tool of manifest.tools) {
@@ -53,6 +58,19 @@ export function validateManifest(manifest: ExtensionManifest): void {
53
58
  );
54
59
  }
55
60
  }
61
+
62
+ if (manifest.connectionCheckTool !== undefined) {
63
+ if (typeof manifest.connectionCheckTool !== "string" || manifest.connectionCheckTool.length === 0) {
64
+ throw new ManifestError("manifest 'connectionCheckTool' must be a non-empty string");
65
+ }
66
+ const checkTool = manifest.tools.find((tool) => tool.name === manifest.connectionCheckTool);
67
+ if (!checkTool || !manifest.connectionCheckTool.startsWith(`${manifest.id}.`)) {
68
+ throw new ManifestError("manifest 'connectionCheckTool' must name a declared tool prefixed by the extension id");
69
+ }
70
+ if ((checkTool.risk ?? "read") !== "read") {
71
+ throw new ManifestError("manifest 'connectionCheckTool' must be read-only");
72
+ }
73
+ }
56
74
  }
57
75
 
58
76
  const FULL_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -274,17 +274,17 @@ export function createIntegrationManager(options: IntegrationManagerOptions) {
274
274
  const recorded = records[agent.id];
275
275
  if (!recorded) {
276
276
  if (!pathPresent(agent.target)) {
277
- return result(agent, "not-installed", "not installed; run `omt integrate` to install");
277
+ return result(agent, "not-installed", "not installed; run `ohmytool integrate` to install");
278
278
  }
279
279
  return result(agent, "conflict", "target exists but is not managed by OMT");
280
280
  }
281
281
  try {
282
282
  assertSafeManagedState(agent, recorded, options.omtHome, platform);
283
283
  } catch {
284
- return result(agent, "conflict", "recorded state is unsafe; run `omt integrate repair`");
284
+ return result(agent, "conflict", "recorded state is unsafe; run `ohmytool integrate repair`");
285
285
  }
286
286
  if (!pathPresent(agent.target) || !safeRealpath(agent.target)) {
287
- return result(agent, "broken", "link is missing; run `omt integrate repair`");
287
+ return result(agent, "broken", "link is missing; run `ohmytool integrate repair`");
288
288
  }
289
289
  if (!isManagedLink(agent.target, recorded.canonical)) {
290
290
  return result(agent, "conflict", "target was replaced by unmanaged content");
@@ -292,7 +292,7 @@ export function createIntegrationManager(options: IntegrationManagerOptions) {
292
292
  if (recorded.version === options.skillVersion) {
293
293
  return result(agent, "current");
294
294
  }
295
- return result(agent, "update-available", `new version ${options.skillVersion} available; run \`omt integrate\``);
295
+ return result(agent, "update-available", `new version ${options.skillVersion} available; run \`ohmytool integrate\``);
296
296
  });
297
297
  }
298
298
 
@@ -81,6 +81,8 @@ export function assertReadOnly(sql: string): void {
81
81
  }
82
82
 
83
83
  const FORBIDDEN_INPUT = new Set([
84
+ "settings",
85
+ "secrets",
84
86
  "host",
85
87
  "username",
86
88
  "password",
@@ -11,6 +11,22 @@ export interface ToolDescriptor {
11
11
  source: { id: string; kind: string; version?: string };
12
12
  }
13
13
 
14
+ export interface ToolSearchOptions {
15
+ readonly limit?: number;
16
+ readonly provider?: string;
17
+ readonly source?: string;
18
+ readonly risk?: ToolDescriptor["risk"];
19
+ }
20
+
21
+ export interface ProviderStatus {
22
+ readonly id: string;
23
+ readonly kind: string;
24
+ readonly status: "available" | "unavailable";
25
+ readonly code?: string;
26
+ readonly message?: string;
27
+ readonly namespace?: string;
28
+ }
29
+
14
30
  export type ToolSearchResult = Omit<ToolDescriptor, "inputSchema">;
15
31
 
16
32
  export interface ExecutionContext {
@@ -22,6 +38,7 @@ export interface ExecutionContext {
22
38
  export interface ToolProvider {
23
39
  readonly id: string;
24
40
  readonly kind: string;
41
+ readonly namespace?: string;
25
42
  listTools(): Promise<readonly ToolDescriptor[]>;
26
43
  execute(toolId: string, input: unknown, context: ExecutionContext): Promise<ToolResult>;
27
44
  close?(): Promise<void>;
@@ -17,6 +17,7 @@ export interface McpProviderOptions {
17
17
  export class McpProvider implements ToolProvider {
18
18
  readonly id: string;
19
19
  readonly kind = "mcp";
20
+ readonly namespace: string;
20
21
  private session?: McpSession;
21
22
  private descriptors?: readonly ToolDescriptor[];
22
23
  private readonly routes = new Map<string, string>();
@@ -24,6 +25,7 @@ export class McpProvider implements ToolProvider {
24
25
 
25
26
  constructor(private readonly options: McpProviderOptions) {
26
27
  this.id = `mcp:${options.serverId}`;
28
+ this.namespace = options.config.namespace;
27
29
  }
28
30
 
29
31
  async listTools(): Promise<readonly ToolDescriptor[]> {
@@ -8,7 +8,15 @@ export class NativeExtensionProvider implements ToolProvider {
8
8
  readonly id = "native";
9
9
  readonly kind = "native";
10
10
 
11
- constructor(private readonly homeOrPaths: string | Pick<OhMyToolPaths, "home">) {}
11
+ private readonly discover: (home: string) => InstalledExtension[];
12
+ private snapshot?: { extensions: InstalledExtension[]; routes: Map<string, InstalledExtension> };
13
+
14
+ constructor(
15
+ private readonly homeOrPaths: string | Pick<OhMyToolPaths, "home">,
16
+ discover: (home: string) => InstalledExtension[] = discoverExtensions,
17
+ ) {
18
+ this.discover = discover;
19
+ }
12
20
 
13
21
  private home(): string {
14
22
  return typeof this.homeOrPaths === "string" ? this.homeOrPaths : this.homeOrPaths.home;
@@ -16,7 +24,7 @@ export class NativeExtensionProvider implements ToolProvider {
16
24
 
17
25
  async listTools(): Promise<readonly ToolDescriptor[]> {
18
26
  const descriptors: ToolDescriptor[] = [];
19
- for (const extension of discoverExtensions(this.home())) {
27
+ for (const extension of this.getSnapshot().extensions) {
20
28
  for (const tool of extension.manifest.tools) {
21
29
  descriptors.push({
22
30
  id: tool.name,
@@ -40,18 +48,34 @@ export class NativeExtensionProvider implements ToolProvider {
40
48
  }
41
49
 
42
50
  async execute(toolId: string, input: unknown, context: ExecutionContext): Promise<ToolResult> {
43
- const extension = this.findExtension(toolId);
51
+ const extension = this.getSnapshot().routes.get(toolId);
52
+ if (!extension) throw new Error(`unknown native tool '${toolId}'`);
44
53
  const definition = await loadExtension(extension);
45
54
  const handler = definition.handlers[toolId];
46
55
  if (!handler) throw new Error(`no handler for ${toolId}`);
47
56
  return handler({ toolName: toolId, logger: context.logger, config: context.config, secrets: context.secrets }, input);
48
57
  }
49
58
 
50
- private findExtension(toolId: string): InstalledExtension {
51
- const extension = discoverExtensions(this.home()).find((candidate) =>
52
- candidate.manifest.tools.some((tool) => tool.name === toolId),
53
- );
54
- if (!extension) throw new Error(`unknown native tool '${toolId}'`);
55
- return extension;
59
+ async hasTool(toolId: string): Promise<boolean> {
60
+ return this.getSnapshot().routes.has(toolId);
61
+ }
62
+
63
+ installedExtensions(): readonly InstalledExtension[] {
64
+ return this.getSnapshot().extensions;
65
+ }
66
+
67
+ extensionForTool(toolId: string): string | undefined {
68
+ return this.getSnapshot().routes.get(toolId)?.manifest.id;
69
+ }
70
+
71
+ private getSnapshot(): { extensions: InstalledExtension[]; routes: Map<string, InstalledExtension> } {
72
+ if (this.snapshot) return this.snapshot;
73
+ const extensions = this.discover(this.home());
74
+ const routes = new Map<string, InstalledExtension>();
75
+ for (const extension of extensions) {
76
+ for (const tool of extension.manifest.tools) routes.set(tool.name, extension);
77
+ }
78
+ this.snapshot = { extensions, routes };
79
+ return this.snapshot;
56
80
  }
57
81
  }
@@ -3,10 +3,17 @@ export interface ToolResult {
3
3
  meta?: Record<string, unknown>;
4
4
  }
5
5
 
6
- export interface ExecutionResult {
7
- ok: boolean;
6
+ export interface ExecutionOk {
7
+ ok: true;
8
8
  toolId: string;
9
- output?: unknown;
10
- meta?: Record<string, unknown>;
11
- error?: { code: string; message: string; details?: unknown };
9
+ output: unknown;
10
+ meta: Record<string, unknown>;
12
11
  }
12
+
13
+ export interface ExecutionError {
14
+ ok: false;
15
+ toolId: string;
16
+ error: { code: string; message: string; details?: unknown };
17
+ }
18
+
19
+ export type ExecutionResult = ExecutionOk | ExecutionError;
@@ -1,4 +1,4 @@
1
- import type { ExecutionContext, ToolDescriptor, ToolProvider, ToolSearchResult } from "./provider";
1
+ import type { ExecutionContext, ProviderStatus, ToolDescriptor, ToolProvider, ToolSearchOptions, ToolSearchResult } from "./provider";
2
2
  import type { ExecutionResult } from "./result";
3
3
  import { executeRuntimeTool, type CreateExecutionContext, type PolicyPreflight } from "./executor";
4
4
  import { RuntimeError } from "./errors";
@@ -20,23 +20,30 @@ interface RuntimeState {
20
20
 
21
21
  export class ToolRuntime {
22
22
  private closePromise?: Promise<void>;
23
+ private readonly discovery = new Map<string, Promise<void>>();
24
+ private readonly statuses = new Map<string, ProviderStatus>();
25
+ private readonly closedProviders = new Set<string>();
23
26
 
24
27
  constructor(private readonly state: RuntimeState, private readonly registeredProviders: readonly ToolProvider[] = []) {}
25
28
 
26
- search(query: string): Promise<ToolSearchResult[]> {
27
- return Promise.resolve(this.state.tools.search(query));
29
+ async search(query: string, options?: ToolSearchOptions): Promise<ToolSearchResult[]> {
30
+ await this.discoverAll();
31
+ return this.state.tools.search(query, options);
28
32
  }
29
33
 
30
- describe(toolId: string): Promise<ToolDescriptor> {
34
+ async describe(toolId: string): Promise<ToolDescriptor> {
35
+ await this.discoverForTarget(toolId);
31
36
  const descriptor = this.state.tools.get(toolId);
32
- if (!descriptor) return Promise.reject(new RuntimeError("TOOL_NOT_FOUND", `unknown tool '${toolId}'`));
33
- return Promise.resolve(descriptor);
37
+ if (!descriptor) throw this.targetError(toolId);
38
+ return descriptor;
34
39
  }
35
40
 
36
41
  async run(toolId: string, input: unknown): Promise<ExecutionResult> {
42
+ await this.discoverForTarget(toolId);
37
43
  const descriptor = this.state.tools.get(toolId);
38
44
  if (!descriptor) {
39
- return { ok: false, toolId, error: { code: "TOOL_NOT_FOUND", message: `unknown tool '${toolId}'` } };
45
+ const error = this.targetError(toolId);
46
+ return { ok: false, toolId, error: { code: error.code, message: error.message } };
40
47
  }
41
48
  const provider = this.state.providers.require(descriptor.provider.id);
42
49
  return executeRuntimeTool({
@@ -47,32 +54,45 @@ export class ToolRuntime {
47
54
  }, (input ?? {}) as Record<string, unknown>);
48
55
  }
49
56
 
57
+ providerStatuses(): readonly ProviderStatus[] {
58
+ return [...this.statuses.values()].sort((a, b) => a.id.localeCompare(b.id));
59
+ }
60
+
50
61
  close(): Promise<void> {
51
62
  if (this.closePromise) return this.closePromise;
52
63
  this.closePromise = (async () => {
53
64
  const errors: unknown[] = [];
54
65
  for (const provider of [...this.registeredProviders].reverse()) {
55
- if (!provider.close) continue;
56
- try { await provider.close(); } catch (error) { errors.push(error); }
66
+ try { await this.closeProvider(provider); } catch (error) { errors.push(error); }
57
67
  }
58
68
  if (errors.length > 0) throw errors[0];
59
69
  })();
60
70
  return this.closePromise;
61
71
  }
62
- }
63
72
 
64
- async function closeProviders(providers: readonly ToolProvider[]): Promise<void> {
65
- await Promise.allSettled([...providers].reverse().map(async (provider) => {
66
- if (provider.close) await provider.close();
67
- }));
68
- }
73
+ private async discoverAll(): Promise<void> {
74
+ await Promise.all(this.registeredProviders.map((provider) => this.discoverProvider(provider)));
75
+ }
69
76
 
70
- export async function createToolRuntime(options: ToolRuntimeOptions): Promise<ToolRuntime> {
71
- const providers = new ProviderRegistry();
72
- const tools = new ToolRegistry();
73
- try {
74
- for (const provider of options.providers) {
75
- providers.register(provider);
77
+ private async discoverForTarget(toolId: string): Promise<void> {
78
+ const nativeProviders = this.registeredProviders.filter((provider) => provider.kind === "native");
79
+ await Promise.all(nativeProviders.map((provider) => this.discoverProvider(provider)));
80
+ if (this.state.tools.get(toolId)) return;
81
+ await Promise.all(this.registeredProviders
82
+ .filter((provider) => provider.kind !== "native")
83
+ .map((provider) => this.discoverProvider(provider)));
84
+ }
85
+
86
+ private discoverProvider(provider: ToolProvider): Promise<void> {
87
+ const current = this.discovery.get(provider.id);
88
+ if (current) return current;
89
+ const promise = this.discoverProviderOnce(provider);
90
+ this.discovery.set(provider.id, promise);
91
+ return promise;
92
+ }
93
+
94
+ private async discoverProviderOnce(provider: ToolProvider): Promise<void> {
95
+ try {
76
96
  const descriptors = await provider.listTools();
77
97
  for (const descriptor of descriptors) {
78
98
  if (descriptor.provider.id !== provider.id || descriptor.provider.kind !== provider.kind) {
@@ -82,12 +102,46 @@ export async function createToolRuntime(options: ToolRuntimeOptions): Promise<To
82
102
  );
83
103
  }
84
104
  }
85
- tools.register(descriptors);
105
+ this.state.tools.register(descriptors);
106
+ this.statuses.set(provider.id, {
107
+ id: provider.id,
108
+ kind: provider.kind,
109
+ status: "available",
110
+ ...("namespace" in provider && typeof provider.namespace === "string" ? { namespace: provider.namespace } : {}),
111
+ });
112
+ } catch (error) {
113
+ if (provider.kind !== "mcp") throw error;
114
+ const typed = error as { code?: unknown; message?: unknown };
115
+ this.statuses.set(provider.id, {
116
+ id: provider.id,
117
+ kind: provider.kind,
118
+ status: "unavailable",
119
+ ...("namespace" in provider && typeof provider.namespace === "string" ? { namespace: provider.namespace } : {}),
120
+ code: typeof typed.code === "string" ? typed.code : "PROVIDER_UNAVAILABLE",
121
+ message: typeof typed.message === "string" ? typed.message : "provider discovery failed",
122
+ });
123
+ try { await this.closeProvider(provider); } catch { /* preserve discovery status */ }
86
124
  }
87
- } catch (error) {
88
- await closeProviders(options.providers);
89
- throw error;
90
125
  }
126
+
127
+ private targetError(toolId: string): RuntimeError {
128
+ const unavailable = this.providerStatuses().find((status) =>
129
+ status.status === "unavailable" && status.namespace !== undefined && toolId.startsWith(`${status.namespace}.`));
130
+ if (unavailable) return new RuntimeError("PROVIDER_UNAVAILABLE", `provider '${unavailable.id}' is unavailable`);
131
+ return new RuntimeError("TOOL_NOT_FOUND", `unknown tool '${toolId}'`);
132
+ }
133
+
134
+ private async closeProvider(provider: ToolProvider): Promise<void> {
135
+ if (!provider.close || this.closedProviders.has(provider.id)) return;
136
+ this.closedProviders.add(provider.id);
137
+ await provider.close();
138
+ }
139
+ }
140
+
141
+ export async function createToolRuntime(options: ToolRuntimeOptions): Promise<ToolRuntime> {
142
+ const providers = new ProviderRegistry();
143
+ const tools = new ToolRegistry();
144
+ for (const provider of options.providers) providers.register(provider);
91
145
  return new ToolRuntime({
92
146
  providers,
93
147
  tools,
@@ -1,9 +1,12 @@
1
- import type { ToolDescriptor, ToolSearchResult } from "./provider";
1
+ import type { ToolDescriptor, ToolSearchOptions, ToolSearchResult } from "./provider";
2
2
  import { RuntimeError } from "./errors";
3
3
 
4
4
  const NAME_WEIGHT = 3;
5
5
  const KEYWORD_WEIGHT = 2;
6
6
  const DESCRIPTION_WEIGHT = 1;
7
+ const EXACT_BOOST = 4;
8
+ const PREFIX_BOOST = 2;
9
+ const MAX_SEARCH_RESULTS = 100;
7
10
 
8
11
  export class ToolRegistry {
9
12
  private readonly tools = new Map<string, ToolDescriptor>();
@@ -25,13 +28,17 @@ export class ToolRegistry {
25
28
  return this.tools.get(toolId);
26
29
  }
27
30
 
28
- search(query: string): ToolSearchResult[] {
31
+ search(query: string, options: ToolSearchOptions = {}): ToolSearchResult[] {
29
32
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
30
33
  if (tokens.length === 0) return [];
31
34
  return [...this.tools.values()]
35
+ .filter((descriptor) => options.provider === undefined || descriptor.provider.id === options.provider)
36
+ .filter((descriptor) => options.source === undefined || descriptor.source.id === options.source)
37
+ .filter((descriptor) => options.risk === undefined || descriptor.risk === options.risk)
32
38
  .map((descriptor) => ({ descriptor, score: this.score(descriptor, tokens) }))
33
39
  .filter((hit) => hit.score > 0)
34
- .sort((a, b) => b.score - a.score)
40
+ .sort((a, b) => b.score - a.score || a.descriptor.id.localeCompare(b.descriptor.id))
41
+ .slice(0, Math.min(Math.max(Math.trunc(options.limit ?? MAX_SEARCH_RESULTS), 1), MAX_SEARCH_RESULTS))
35
42
  .map(({ descriptor }) => {
36
43
  const { inputSchema: _inputSchema, ...summary } = descriptor;
37
44
  return summary;
@@ -43,12 +50,20 @@ export class ToolRegistry {
43
50
  const description = descriptor.description.toLowerCase();
44
51
  const keywords = (descriptor.keywords ?? []).map((keyword) => keyword.toLowerCase());
45
52
  return tokens.reduce((score, token) => {
46
- if (id.includes(token)) return score + NAME_WEIGHT;
47
- if (keywords.some((keyword) => keyword.includes(token) || token.includes(keyword))) {
48
- return score + KEYWORD_WEIGHT;
49
- }
50
- if (description.includes(token)) return score + DESCRIPTION_WEIGHT;
53
+ const idScore = matchWeight(id, token, NAME_WEIGHT);
54
+ if (idScore > 0) return score + idScore;
55
+ const keywordScore = Math.max(0, ...keywords.map((keyword) => matchWeight(keyword, token, KEYWORD_WEIGHT)));
56
+ if (keywordScore > 0) return score + keywordScore;
57
+ const descriptionScore = matchWeight(description, token, DESCRIPTION_WEIGHT);
58
+ if (descriptionScore > 0) return score + descriptionScore;
51
59
  return score;
52
60
  }, 0);
53
61
  }
54
62
  }
63
+
64
+ function matchWeight(value: string, token: string, base: number): number {
65
+ if (value === token) return base + EXACT_BOOST;
66
+ if (value.startsWith(token)) return base + PREFIX_BOOST;
67
+ if (value.includes(token) || token.includes(value)) return base;
68
+ return 0;
69
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.3.2";
1
+ export const VERSION = "0.3.4";
@@ -1,99 +0,0 @@
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, sanitizeExtensionConnections } 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 ?? { connections: sanitizeExtensionConnections(deps.config)[extension.id] ?? {} }) 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
-
@@ -1,33 +0,0 @@
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
- }
@@ -1,14 +0,0 @@
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;
@@ -1,2 +0,0 @@
1
- export { validateInput } from "../runtime/schema";
2
- export type { Schema } from "../runtime/schema";
@@ -1,78 +0,0 @@
1
- import type { ExtensionManifest } from "@oh-my-tool/sdk";
2
-
3
- export interface SearchHit {
4
- name: string;
5
- description: string;
6
- extension: string;
7
- risk: string;
8
- score: number;
9
- }
10
-
11
- interface Searchable {
12
- name: string;
13
- description: string;
14
- keywords: string[];
15
- risk: string;
16
- extension: string;
17
- extName: string;
18
- }
19
-
20
- const NAME_WEIGHT = 3;
21
- const KEYWORD_WEIGHT = 2;
22
- const DESC_WEIGHT = 1;
23
-
24
- function toSearchable(ext: ExtensionManifest) {
25
- const out: Searchable[] = [];
26
- for (const tool of ext.tools) {
27
- out.push({
28
- name: tool.name.toLowerCase(),
29
- description: tool.description.toLowerCase(),
30
- keywords: (tool.keywords ?? []).map((k) => k.toLowerCase()),
31
- risk: tool.risk ?? "read",
32
- extension: ext.id,
33
- extName: ext.name.toLowerCase(),
34
- });
35
- }
36
- return out;
37
- }
38
-
39
- function bestWeight(s: Searchable, token: string): number {
40
- if (s.name.includes(token)) return NAME_WEIGHT;
41
- if (s.extName.includes(token)) return NAME_WEIGHT;
42
- if (s.keywords.some((k) => k.includes(token) || token.includes(k))) {
43
- return KEYWORD_WEIGHT;
44
- }
45
- if (s.description.includes(token)) return DESC_WEIGHT;
46
- return 0;
47
- }
48
-
49
- export function searchTools(query: string, manifests: ExtensionManifest[]): SearchHit[] {
50
- const tokens = query
51
- .toLowerCase()
52
- .split(/\s+/)
53
- .filter((t) => t.length > 0);
54
-
55
- if (tokens.length === 0) return [];
56
-
57
- const hits: SearchHit[] = [];
58
- for (const ext of manifests) {
59
- for (const s of toSearchable(ext)) {
60
- let score = 0;
61
- for (const token of tokens) {
62
- score += bestWeight(s, token);
63
- }
64
- if (score > 0) {
65
- hits.push({
66
- name: s.name,
67
- description: s.description,
68
- extension: s.extension,
69
- risk: s.risk,
70
- score,
71
- });
72
- }
73
- }
74
- }
75
-
76
- hits.sort((a, b) => b.score - a.score);
77
- return hits;
78
- }