@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
|
@@ -1,62 +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;
|
|
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
62
|
}
|
package/src/extension/install.ts
CHANGED
|
@@ -1,24 +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 };
|
|
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
24
|
}
|
package/src/extension/loader.ts
CHANGED
|
@@ -1,32 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
import { OMT_API_VERSION, type ExtensionManifest } from "@oh-my-tool/sdk";
|
|
2
|
-
|
|
3
|
-
export class ManifestError extends Error {}
|
|
4
|
-
|
|
5
|
-
export function parseManifest(raw: string): ExtensionManifest {
|
|
6
|
-
let data: unknown;
|
|
7
|
-
try {
|
|
8
|
-
data = JSON.parse(raw);
|
|
9
|
-
} catch {
|
|
10
|
-
throw new ManifestError("manifest is not valid JSON");
|
|
11
|
-
}
|
|
12
|
-
const obj = data as Partial<ExtensionManifest>;
|
|
13
|
-
if (!obj.id || typeof obj.id !== "string") {
|
|
14
|
-
throw new ManifestError("manifest must contain a string 'id'");
|
|
15
|
-
}
|
|
16
|
-
if (!Array.isArray(obj.tools)) {
|
|
17
|
-
throw new ManifestError("manifest must contain a 'tools' array");
|
|
18
|
-
}
|
|
19
|
-
return obj as ExtensionManifest;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function validateManifest(manifest: ExtensionManifest): void {
|
|
23
|
-
if (!manifest.name || typeof manifest.name !== "string") {
|
|
24
|
-
throw new ManifestError("manifest must contain a string 'name'");
|
|
25
|
-
}
|
|
26
|
-
if (!manifest.version || typeof manifest.version !== "string") {
|
|
27
|
-
throw new ManifestError("manifest must contain a string 'version'");
|
|
28
|
-
}
|
|
29
|
-
if (!manifest.sdkVersion || typeof manifest.sdkVersion !== "string") {
|
|
30
|
-
throw new ManifestError("manifest must contain a string 'sdkVersion'");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const seen = new Set<string>();
|
|
34
|
-
for (const tool of manifest.tools) {
|
|
35
|
-
if (!tool.name || typeof tool.name !== "string") {
|
|
36
|
-
throw new ManifestError("each tool must have a string 'name'");
|
|
37
|
-
}
|
|
38
|
-
if (seen.has(tool.name)) {
|
|
39
|
-
throw new ManifestError(`duplicate tool name '${tool.name}'`);
|
|
40
|
-
}
|
|
41
|
-
seen.add(tool.name);
|
|
42
|
-
|
|
43
|
-
const prefix = `${manifest.id}.`;
|
|
44
|
-
if (!tool.name.startsWith(prefix)) {
|
|
45
|
-
throw new ManifestError(
|
|
46
|
-
`tool '${tool.name}' must be prefixed by extension id '${manifest.id}.'`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const FULL_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
53
|
-
const PARTIAL_VERSION_RE = /^(\d+|x|X|\*)(\.(\d+|x|X|\*))?(\.(\d+|x|X|\*))?$/;
|
|
54
|
-
|
|
55
|
-
function isValidVersionToken(token: string): boolean {
|
|
56
|
-
return FULL_VERSION_RE.test(token) || PARTIAL_VERSION_RE.test(token);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isValidSemverRange(range: string): boolean {
|
|
60
|
-
const trimmed = range.trim();
|
|
61
|
-
if (!trimmed) return false;
|
|
62
|
-
for (const orPart of trimmed.split("||")) {
|
|
63
|
-
const pieces = orPart.trim().split(/\s+/).filter(Boolean);
|
|
64
|
-
if (pieces.length === 0) return false;
|
|
65
|
-
for (let i = 0; i < pieces.length; i++) {
|
|
66
|
-
const piece = pieces[i];
|
|
67
|
-
if (piece === "-") {
|
|
68
|
-
if (i === 0 || i === pieces.length - 1) return false;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const m = piece.match(/^(>=|<=|>|<|=|\^|~)?(.*)$/);
|
|
72
|
-
if (!m || m[2] === "" || !isValidVersionToken(m[2])) return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function checkSdkCompatibility(requiredRange: string): void {
|
|
79
|
-
if (typeof requiredRange !== "string" || requiredRange.trim() === "") {
|
|
80
|
-
throw new ManifestError("extension must declare a non-empty 'sdkVersion'");
|
|
81
|
-
}
|
|
82
|
-
if (!isValidSemverRange(requiredRange)) {
|
|
83
|
-
throw new ManifestError(
|
|
84
|
-
`invalid sdkVersion '${requiredRange}': not a valid semver range`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
if (!Bun.semver.satisfies(OMT_API_VERSION, requiredRange)) {
|
|
88
|
-
throw new ManifestError(
|
|
89
|
-
`extension requires sdk '${requiredRange}' but core provides '${OMT_API_VERSION}'`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function validateHandlers(
|
|
95
|
-
manifest: ExtensionManifest,
|
|
96
|
-
handlerNames: string[],
|
|
97
|
-
): void {
|
|
98
|
-
const handlerSet = new Set(handlerNames);
|
|
99
|
-
const manifestSet = new Set(manifest.tools.map((t) => t.name));
|
|
100
|
-
|
|
101
|
-
for (const name of manifest.tools.map((t) => t.name)) {
|
|
102
|
-
if (!handlerSet.has(name)) {
|
|
103
|
-
throw new ManifestError(
|
|
104
|
-
`manifest declares tool '${name}' but runtime has no handler`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
for (const name of handlerNames) {
|
|
109
|
-
if (!manifestSet.has(name)) {
|
|
110
|
-
throw new ManifestError(
|
|
111
|
-
`runtime registers tool '${name}' not declared in manifest`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
1
|
+
import { OMT_API_VERSION, type ExtensionManifest } from "@oh-my-tool/sdk";
|
|
2
|
+
|
|
3
|
+
export class ManifestError extends Error {}
|
|
4
|
+
|
|
5
|
+
export function parseManifest(raw: string): ExtensionManifest {
|
|
6
|
+
let data: unknown;
|
|
7
|
+
try {
|
|
8
|
+
data = JSON.parse(raw);
|
|
9
|
+
} catch {
|
|
10
|
+
throw new ManifestError("manifest is not valid JSON");
|
|
11
|
+
}
|
|
12
|
+
const obj = data as Partial<ExtensionManifest>;
|
|
13
|
+
if (!obj.id || typeof obj.id !== "string") {
|
|
14
|
+
throw new ManifestError("manifest must contain a string 'id'");
|
|
15
|
+
}
|
|
16
|
+
if (!Array.isArray(obj.tools)) {
|
|
17
|
+
throw new ManifestError("manifest must contain a 'tools' array");
|
|
18
|
+
}
|
|
19
|
+
return obj as ExtensionManifest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateManifest(manifest: ExtensionManifest): void {
|
|
23
|
+
if (!manifest.name || typeof manifest.name !== "string") {
|
|
24
|
+
throw new ManifestError("manifest must contain a string 'name'");
|
|
25
|
+
}
|
|
26
|
+
if (!manifest.version || typeof manifest.version !== "string") {
|
|
27
|
+
throw new ManifestError("manifest must contain a string 'version'");
|
|
28
|
+
}
|
|
29
|
+
if (!manifest.sdkVersion || typeof manifest.sdkVersion !== "string") {
|
|
30
|
+
throw new ManifestError("manifest must contain a string 'sdkVersion'");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
for (const tool of manifest.tools) {
|
|
35
|
+
if (!tool.name || typeof tool.name !== "string") {
|
|
36
|
+
throw new ManifestError("each tool must have a string 'name'");
|
|
37
|
+
}
|
|
38
|
+
if (seen.has(tool.name)) {
|
|
39
|
+
throw new ManifestError(`duplicate tool name '${tool.name}'`);
|
|
40
|
+
}
|
|
41
|
+
seen.add(tool.name);
|
|
42
|
+
|
|
43
|
+
const prefix = `${manifest.id}.`;
|
|
44
|
+
if (!tool.name.startsWith(prefix)) {
|
|
45
|
+
throw new ManifestError(
|
|
46
|
+
`tool '${tool.name}' must be prefixed by extension id '${manifest.id}.'`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const FULL_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
53
|
+
const PARTIAL_VERSION_RE = /^(\d+|x|X|\*)(\.(\d+|x|X|\*))?(\.(\d+|x|X|\*))?$/;
|
|
54
|
+
|
|
55
|
+
function isValidVersionToken(token: string): boolean {
|
|
56
|
+
return FULL_VERSION_RE.test(token) || PARTIAL_VERSION_RE.test(token);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidSemverRange(range: string): boolean {
|
|
60
|
+
const trimmed = range.trim();
|
|
61
|
+
if (!trimmed) return false;
|
|
62
|
+
for (const orPart of trimmed.split("||")) {
|
|
63
|
+
const pieces = orPart.trim().split(/\s+/).filter(Boolean);
|
|
64
|
+
if (pieces.length === 0) return false;
|
|
65
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
66
|
+
const piece = pieces[i];
|
|
67
|
+
if (piece === "-") {
|
|
68
|
+
if (i === 0 || i === pieces.length - 1) return false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const m = piece.match(/^(>=|<=|>|<|=|\^|~)?(.*)$/);
|
|
72
|
+
if (!m || m[2] === "" || !isValidVersionToken(m[2])) return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function checkSdkCompatibility(requiredRange: string): void {
|
|
79
|
+
if (typeof requiredRange !== "string" || requiredRange.trim() === "") {
|
|
80
|
+
throw new ManifestError("extension must declare a non-empty 'sdkVersion'");
|
|
81
|
+
}
|
|
82
|
+
if (!isValidSemverRange(requiredRange)) {
|
|
83
|
+
throw new ManifestError(
|
|
84
|
+
`invalid sdkVersion '${requiredRange}': not a valid semver range`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!Bun.semver.satisfies(OMT_API_VERSION, requiredRange)) {
|
|
88
|
+
throw new ManifestError(
|
|
89
|
+
`extension requires sdk '${requiredRange}' but core provides '${OMT_API_VERSION}'`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validateHandlers(
|
|
95
|
+
manifest: ExtensionManifest,
|
|
96
|
+
handlerNames: string[],
|
|
97
|
+
): void {
|
|
98
|
+
const handlerSet = new Set(handlerNames);
|
|
99
|
+
const manifestSet = new Set(manifest.tools.map((t) => t.name));
|
|
100
|
+
|
|
101
|
+
for (const name of manifest.tools.map((t) => t.name)) {
|
|
102
|
+
if (!handlerSet.has(name)) {
|
|
103
|
+
throw new ManifestError(
|
|
104
|
+
`manifest declares tool '${name}' but runtime has no handler`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const name of handlerNames) {
|
|
109
|
+
if (!manifestSet.has(name)) {
|
|
110
|
+
throw new ManifestError(
|
|
111
|
+
`runtime registers tool '${name}' not declared in manifest`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
115
|
}
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { basename, delimiter, extname, join } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import type { AgentDetection, AgentId } from "./types";
|
|
5
|
-
|
|
6
|
-
export type FindCommand = (candidates: string[]) => Promise<string | undefined> | string | undefined;
|
|
7
|
-
|
|
8
|
-
export interface DetectionOptions {
|
|
9
|
-
userHome?: string;
|
|
10
|
-
findCommand?: FindCommand;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface AdapterDefinition {
|
|
14
|
-
id: AgentId;
|
|
15
|
-
displayName: string;
|
|
16
|
-
commands: string[];
|
|
17
|
-
target(userHome: string, command: string): string;
|
|
18
|
-
variant?(command: string): string | undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const definitions: AdapterDefinition[] = [
|
|
22
|
-
{
|
|
23
|
-
id: "codex",
|
|
24
|
-
displayName: "Codex",
|
|
25
|
-
commands: ["codex"],
|
|
26
|
-
target: (home) => join(home, ".agents", "skills", "oh-my-tool"),
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
id: "omp",
|
|
30
|
-
displayName: "OMP",
|
|
31
|
-
commands: ["omp"],
|
|
32
|
-
target: (home) => join(home, ".omp", "agent", "skills", "oh-my-tool"),
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: "qoder",
|
|
36
|
-
displayName: "Qoder",
|
|
37
|
-
commands: ["qoderclicn", "qodercli", "qoder"],
|
|
38
|
-
target: (home, command) =>
|
|
39
|
-
commandName(command) === "qoderclicn"
|
|
40
|
-
? join(home, ".qoder-cn", "skills", "oh-my-tool")
|
|
41
|
-
: join(home, ".qoder", "skills", "oh-my-tool"),
|
|
42
|
-
variant: (command) => (commandName(command) === "qoderclicn" ? "Qoder CLI CN" : undefined),
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "pi",
|
|
46
|
-
displayName: "Pi",
|
|
47
|
-
commands: ["pi"],
|
|
48
|
-
target: (home) => join(home, ".agents", "skills", "oh-my-tool"),
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "cursor",
|
|
52
|
-
displayName: "Cursor",
|
|
53
|
-
commands: ["cursor"],
|
|
54
|
-
target: (home) => join(home, ".agents", "skills", "oh-my-tool"),
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: "claude",
|
|
58
|
-
displayName: "Claude Code",
|
|
59
|
-
commands: ["claude"],
|
|
60
|
-
target: (home) => join(home, ".claude", "skills", "oh-my-tool"),
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
function commandName(command: string): string {
|
|
65
|
-
return basename(command, extname(command)).toLowerCase();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function findCommandOnPath(candidates: string[]): string | undefined {
|
|
69
|
-
const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
70
|
-
const suffixes = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
|
|
71
|
-
for (const candidate of candidates) {
|
|
72
|
-
for (const directory of pathEntries) {
|
|
73
|
-
for (const suffix of suffixes) {
|
|
74
|
-
const path = join(directory, candidate + suffix);
|
|
75
|
-
if (existsSync(path)) return path;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function detectAgents(options: DetectionOptions = {}): Promise<AgentDetection[]> {
|
|
83
|
-
const userHome = options.userHome ?? homedir();
|
|
84
|
-
const findCommand = options.findCommand ?? findCommandOnPath;
|
|
85
|
-
const detected: AgentDetection[] = [];
|
|
86
|
-
for (const definition of definitions) {
|
|
87
|
-
const command = await findCommand(definition.commands);
|
|
88
|
-
if (!command) continue;
|
|
89
|
-
detected.push({
|
|
90
|
-
id: definition.id,
|
|
91
|
-
displayName: definition.displayName,
|
|
92
|
-
command,
|
|
93
|
-
target: definition.target(userHome, command),
|
|
94
|
-
variant: definition.variant?.(command),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return detected;
|
|
98
|
-
}
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { basename, delimiter, extname, join } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import type { AgentDetection, AgentId } from "./types";
|
|
5
|
+
|
|
6
|
+
export type FindCommand = (candidates: string[]) => Promise<string | undefined> | string | undefined;
|
|
7
|
+
|
|
8
|
+
export interface DetectionOptions {
|
|
9
|
+
userHome?: string;
|
|
10
|
+
findCommand?: FindCommand;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AdapterDefinition {
|
|
14
|
+
id: AgentId;
|
|
15
|
+
displayName: string;
|
|
16
|
+
commands: string[];
|
|
17
|
+
target(userHome: string, command: string): string;
|
|
18
|
+
variant?(command: string): string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const definitions: AdapterDefinition[] = [
|
|
22
|
+
{
|
|
23
|
+
id: "codex",
|
|
24
|
+
displayName: "Codex",
|
|
25
|
+
commands: ["codex"],
|
|
26
|
+
target: (home) => join(home, ".agents", "skills", "oh-my-tool"),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "omp",
|
|
30
|
+
displayName: "OMP",
|
|
31
|
+
commands: ["omp"],
|
|
32
|
+
target: (home) => join(home, ".omp", "agent", "skills", "oh-my-tool"),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "qoder",
|
|
36
|
+
displayName: "Qoder",
|
|
37
|
+
commands: ["qoderclicn", "qodercli", "qoder"],
|
|
38
|
+
target: (home, command) =>
|
|
39
|
+
commandName(command) === "qoderclicn"
|
|
40
|
+
? join(home, ".qoder-cn", "skills", "oh-my-tool")
|
|
41
|
+
: join(home, ".qoder", "skills", "oh-my-tool"),
|
|
42
|
+
variant: (command) => (commandName(command) === "qoderclicn" ? "Qoder CLI CN" : undefined),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "pi",
|
|
46
|
+
displayName: "Pi",
|
|
47
|
+
commands: ["pi"],
|
|
48
|
+
target: (home) => join(home, ".agents", "skills", "oh-my-tool"),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "cursor",
|
|
52
|
+
displayName: "Cursor",
|
|
53
|
+
commands: ["cursor"],
|
|
54
|
+
target: (home) => join(home, ".agents", "skills", "oh-my-tool"),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "claude",
|
|
58
|
+
displayName: "Claude Code",
|
|
59
|
+
commands: ["claude"],
|
|
60
|
+
target: (home) => join(home, ".claude", "skills", "oh-my-tool"),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function commandName(command: string): string {
|
|
65
|
+
return basename(command, extname(command)).toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function findCommandOnPath(candidates: string[]): string | undefined {
|
|
69
|
+
const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
70
|
+
const suffixes = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
for (const directory of pathEntries) {
|
|
73
|
+
for (const suffix of suffixes) {
|
|
74
|
+
const path = join(directory, candidate + suffix);
|
|
75
|
+
if (existsSync(path)) return path;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function detectAgents(options: DetectionOptions = {}): Promise<AgentDetection[]> {
|
|
83
|
+
const userHome = options.userHome ?? homedir();
|
|
84
|
+
const findCommand = options.findCommand ?? findCommandOnPath;
|
|
85
|
+
const detected: AgentDetection[] = [];
|
|
86
|
+
for (const definition of definitions) {
|
|
87
|
+
const command = await findCommand(definition.commands);
|
|
88
|
+
if (!command) continue;
|
|
89
|
+
detected.push({
|
|
90
|
+
id: definition.id,
|
|
91
|
+
displayName: definition.displayName,
|
|
92
|
+
command,
|
|
93
|
+
target: definition.target(userHome, command),
|
|
94
|
+
variant: definition.variant?.(command),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return detected;
|
|
98
|
+
}
|