@openkeyai/tool-manifest 0.1.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.
@@ -0,0 +1,110 @@
1
+ import { ToolManifest } from './schema.cjs';
2
+ export { ProviderSlug, ProviderSlugEnum, RuntimeTier, RuntimeTierEnum, ToolManifestSchema, ToolOwner, ToolOwnerSchema, ToolScope, ToolScopeEnum } from './schema.cjs';
3
+ export { ZodError } from 'zod';
4
+
5
+ /**
6
+ * Parse + validate a `tool.json` payload. Pass either the parsed object or
7
+ * the raw JSON string; we handle both.
8
+ *
9
+ * Returns the typed manifest on success.
10
+ * Throws `ZodError` on schema failure (caller can `instanceof ZodError`).
11
+ * Throws `Error` on JSON parse failure.
12
+ */
13
+ declare function validateManifest(input: unknown): ToolManifest;
14
+ /**
15
+ * Convenience — read + parse + validate a tool.json from disk.
16
+ *
17
+ * Used by the CLI and by the auto-scaffolder. Returns the manifest on
18
+ * success; throws otherwise.
19
+ */
20
+ declare function validateManifestFile(path: string): ToolManifest;
21
+
22
+ /**
23
+ * Source scanner.
24
+ *
25
+ * Walks a tool's source tree and returns a list of contract violations.
26
+ * The CLI (`okai-scan`) wraps this; tool CI calls it directly.
27
+ *
28
+ * Design notes:
29
+ *
30
+ * - **Regex / text scanning over AST.** A real TS AST parse (ts-morph)
31
+ * catches more edge cases but adds ~5MB of deps and noticeable cold-
32
+ * start time on every tool's CI. The patterns we enforce in v1 are
33
+ * simple string matches; we're catching obvious violations, not
34
+ * adversarial code. If we ever need to enforce something subtle (e.g.
35
+ * "you mounted HubHeader but inside a `{false && ...}` branch") we
36
+ * can swap the backend later — the public function signature won't
37
+ * change.
38
+ *
39
+ * - **Heuristic file matching.** We treat any file matching
40
+ * `**\/layout.{ts,tsx,js,jsx,mjs,cjs}` as a "layout" candidate, which
41
+ * covers both Next.js App Router (`app/layout.tsx`) and the variants
42
+ * containers use (`src/layout.tsx`, etc).
43
+ *
44
+ * - **Single-pass walk.** Bigger tools have hundreds of source files;
45
+ * doing one walk and routing each file to all rules keeps it linear.
46
+ *
47
+ * - **No file IO outside `srcDir`.** Safe to point at any directory;
48
+ * we don't write anything.
49
+ */
50
+ type ScannerViolation = {
51
+ /** Stable code so tool authors can suppress / look up. */
52
+ code: "missing-hubheader-import" | "missing-hubheader-mount" | "banned-internal-import" | "manifest-missing" | "manifest-invalid" | "io-error";
53
+ /** Human-readable. Surface this in CI logs. */
54
+ message: string;
55
+ /** Relative path inside `srcDir` (or `tool.json` for manifest issues). */
56
+ file?: string;
57
+ /** 1-based line number for source-file violations. */
58
+ line?: number;
59
+ };
60
+ type ScanInput = {
61
+ /**
62
+ * Directory containing the tool's source code (usually `src/` or `app/`
63
+ * or the repo root). The scanner walks recursively.
64
+ */
65
+ srcDir: string;
66
+ /**
67
+ * Optional path to `tool.json`. If provided, the manifest is loaded and
68
+ * validated against the schema; failures are reported as scanner
69
+ * violations. Defaults to no manifest check (you can call
70
+ * `validateManifestFile` directly).
71
+ */
72
+ manifestPath?: string;
73
+ };
74
+ type ScanResult = {
75
+ violations: ScannerViolation[];
76
+ /** Files actually walked — useful for CI logs. */
77
+ scannedFileCount: number;
78
+ };
79
+ /**
80
+ * Scan a tool's source tree for contract violations.
81
+ *
82
+ * This function makes no assumptions about the tool's framework. It just
83
+ * walks `srcDir`, applies a small set of rules, and reports what it sees.
84
+ */
85
+ declare function scanToolSource(input: ScanInput): ScanResult;
86
+
87
+ /**
88
+ * `@openkeyai/tool-manifest` — public entry.
89
+ *
90
+ * Three surfaces:
91
+ *
92
+ * 1. The Zod schema (`ToolManifestSchema`) — frozen shape of every
93
+ * tool's `tool.json`. Re-exported via the `./schema` subpath too,
94
+ * so consumers that ONLY need the schema can avoid pulling the
95
+ * filesystem-touching scanner code.
96
+ *
97
+ * 2. `validateManifest(json)` / `validateManifestFile(path)` —
98
+ * convenience wrappers around `ToolManifestSchema.parse`.
99
+ *
100
+ * 3. `scanToolSource({ srcDir, manifestPath? })` — walks the tool's
101
+ * source tree and returns contract violations. Used by tool CI
102
+ * and the auto-scaffolder.
103
+ *
104
+ * The CLI (`okai-scan`) is a thin wrapper around these — `bin/cli.ts`.
105
+ */
106
+
107
+ /** Bumped on each release. */
108
+ declare const TOOL_MANIFEST_VERSION = "0.1.0";
109
+
110
+ export { type ScanInput, type ScanResult, type ScannerViolation, TOOL_MANIFEST_VERSION, ToolManifest, scanToolSource, validateManifest, validateManifestFile };
@@ -0,0 +1,110 @@
1
+ import { ToolManifest } from './schema.js';
2
+ export { ProviderSlug, ProviderSlugEnum, RuntimeTier, RuntimeTierEnum, ToolManifestSchema, ToolOwner, ToolOwnerSchema, ToolScope, ToolScopeEnum } from './schema.js';
3
+ export { ZodError } from 'zod';
4
+
5
+ /**
6
+ * Parse + validate a `tool.json` payload. Pass either the parsed object or
7
+ * the raw JSON string; we handle both.
8
+ *
9
+ * Returns the typed manifest on success.
10
+ * Throws `ZodError` on schema failure (caller can `instanceof ZodError`).
11
+ * Throws `Error` on JSON parse failure.
12
+ */
13
+ declare function validateManifest(input: unknown): ToolManifest;
14
+ /**
15
+ * Convenience — read + parse + validate a tool.json from disk.
16
+ *
17
+ * Used by the CLI and by the auto-scaffolder. Returns the manifest on
18
+ * success; throws otherwise.
19
+ */
20
+ declare function validateManifestFile(path: string): ToolManifest;
21
+
22
+ /**
23
+ * Source scanner.
24
+ *
25
+ * Walks a tool's source tree and returns a list of contract violations.
26
+ * The CLI (`okai-scan`) wraps this; tool CI calls it directly.
27
+ *
28
+ * Design notes:
29
+ *
30
+ * - **Regex / text scanning over AST.** A real TS AST parse (ts-morph)
31
+ * catches more edge cases but adds ~5MB of deps and noticeable cold-
32
+ * start time on every tool's CI. The patterns we enforce in v1 are
33
+ * simple string matches; we're catching obvious violations, not
34
+ * adversarial code. If we ever need to enforce something subtle (e.g.
35
+ * "you mounted HubHeader but inside a `{false && ...}` branch") we
36
+ * can swap the backend later — the public function signature won't
37
+ * change.
38
+ *
39
+ * - **Heuristic file matching.** We treat any file matching
40
+ * `**\/layout.{ts,tsx,js,jsx,mjs,cjs}` as a "layout" candidate, which
41
+ * covers both Next.js App Router (`app/layout.tsx`) and the variants
42
+ * containers use (`src/layout.tsx`, etc).
43
+ *
44
+ * - **Single-pass walk.** Bigger tools have hundreds of source files;
45
+ * doing one walk and routing each file to all rules keeps it linear.
46
+ *
47
+ * - **No file IO outside `srcDir`.** Safe to point at any directory;
48
+ * we don't write anything.
49
+ */
50
+ type ScannerViolation = {
51
+ /** Stable code so tool authors can suppress / look up. */
52
+ code: "missing-hubheader-import" | "missing-hubheader-mount" | "banned-internal-import" | "manifest-missing" | "manifest-invalid" | "io-error";
53
+ /** Human-readable. Surface this in CI logs. */
54
+ message: string;
55
+ /** Relative path inside `srcDir` (or `tool.json` for manifest issues). */
56
+ file?: string;
57
+ /** 1-based line number for source-file violations. */
58
+ line?: number;
59
+ };
60
+ type ScanInput = {
61
+ /**
62
+ * Directory containing the tool's source code (usually `src/` or `app/`
63
+ * or the repo root). The scanner walks recursively.
64
+ */
65
+ srcDir: string;
66
+ /**
67
+ * Optional path to `tool.json`. If provided, the manifest is loaded and
68
+ * validated against the schema; failures are reported as scanner
69
+ * violations. Defaults to no manifest check (you can call
70
+ * `validateManifestFile` directly).
71
+ */
72
+ manifestPath?: string;
73
+ };
74
+ type ScanResult = {
75
+ violations: ScannerViolation[];
76
+ /** Files actually walked — useful for CI logs. */
77
+ scannedFileCount: number;
78
+ };
79
+ /**
80
+ * Scan a tool's source tree for contract violations.
81
+ *
82
+ * This function makes no assumptions about the tool's framework. It just
83
+ * walks `srcDir`, applies a small set of rules, and reports what it sees.
84
+ */
85
+ declare function scanToolSource(input: ScanInput): ScanResult;
86
+
87
+ /**
88
+ * `@openkeyai/tool-manifest` — public entry.
89
+ *
90
+ * Three surfaces:
91
+ *
92
+ * 1. The Zod schema (`ToolManifestSchema`) — frozen shape of every
93
+ * tool's `tool.json`. Re-exported via the `./schema` subpath too,
94
+ * so consumers that ONLY need the schema can avoid pulling the
95
+ * filesystem-touching scanner code.
96
+ *
97
+ * 2. `validateManifest(json)` / `validateManifestFile(path)` —
98
+ * convenience wrappers around `ToolManifestSchema.parse`.
99
+ *
100
+ * 3. `scanToolSource({ srcDir, manifestPath? })` — walks the tool's
101
+ * source tree and returns contract violations. Used by tool CI
102
+ * and the auto-scaffolder.
103
+ *
104
+ * The CLI (`okai-scan`) is a thin wrapper around these — `bin/cli.ts`.
105
+ */
106
+
107
+ /** Bumped on each release. */
108
+ declare const TOOL_MANIFEST_VERSION = "0.1.0";
109
+
110
+ export { type ScanInput, type ScanResult, type ScannerViolation, TOOL_MANIFEST_VERSION, ToolManifest, scanToolSource, validateManifest, validateManifestFile };
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ import { z } from 'zod';
2
+ export { ZodError } from 'zod';
3
+ import { readFileSync, statSync, readdirSync } from 'fs';
4
+ import { join, relative } from 'path';
5
+
6
+ // src/schema.ts
7
+ var slugRegex = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
8
+ var ToolScopeEnum = z.enum(["keys.read", "user.read", "billing.read"]);
9
+ var ProviderSlugEnum = z.enum([
10
+ "openai",
11
+ "anthropic",
12
+ "google",
13
+ "replicate",
14
+ "elevenlabs",
15
+ "fal"
16
+ ]);
17
+ var RuntimeTierEnum = z.enum(["edge", "container"]);
18
+ var ToolOwnerSchema = z.object({
19
+ name: z.string().min(1).max(80),
20
+ email: z.string().email().optional(),
21
+ github: z.string().regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}[a-zA-Z0-9])?$/, "github handle").optional()
22
+ });
23
+ var ToolManifestSchema = z.object({
24
+ /** Stable URL-safe identifier. Becomes the JWT's `aud` claim. */
25
+ slug: z.string().regex(slugRegex, "slug must be lowercase kebab-case, 3\u201340 chars"),
26
+ /** Human-readable name shown in the catalog and HubHeader. */
27
+ name: z.string().min(1).max(80),
28
+ /** One-sentence catalog description. */
29
+ description: z.string().max(280).default(""),
30
+ /** Tool version (semver). */
31
+ version: z.string().regex(/^\d+\.\d+\.\d+(?:-[\w.+-]+)?$/, "semver"),
32
+ /** Runtime tier — picks template + deploy pipeline. */
33
+ runtime: RuntimeTierEnum,
34
+ /** Scopes the tool may request when minting a JWT. Min 1. */
35
+ scopes: z.array(ToolScopeEnum).min(1),
36
+ /** API provider slugs the tool may fetch keys for. */
37
+ providers: z.array(ProviderSlugEnum).default([]),
38
+ /** Public homepage / docs URL. Surfaced in the hub catalog. */
39
+ homepage: z.string().url().optional(),
40
+ /** Where the hub sends users after issuing a token. https only. */
41
+ callback_url: z.string().url().refine((u) => u.startsWith("https://"), "callback_url must be https://"),
42
+ /** Tool owner. */
43
+ owner: ToolOwnerSchema,
44
+ /** Semver range the tool was built against. Hub uses this for compat warnings. */
45
+ sdk_version: z.string().min(1),
46
+ /** Optional category for catalog grouping (e.g. "media", "writing"). */
47
+ category: z.string().min(1).max(40).optional()
48
+ });
49
+ function validateManifest(input) {
50
+ let json;
51
+ if (typeof input === "string") {
52
+ json = JSON.parse(input);
53
+ } else {
54
+ json = input;
55
+ }
56
+ return ToolManifestSchema.parse(json);
57
+ }
58
+ function validateManifestFile(path) {
59
+ const raw = readFileSync(path, "utf8");
60
+ return validateManifest(raw);
61
+ }
62
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
63
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
64
+ "node_modules",
65
+ "dist",
66
+ ".next",
67
+ ".open-next",
68
+ ".turbo",
69
+ "build",
70
+ "out",
71
+ "coverage",
72
+ ".git"
73
+ ]);
74
+ var PATTERNS = {
75
+ /** ESM/CJS import OR a `require()` of @openkeyai/ui. */
76
+ uiImport: /\bfrom\s+['"]@openkeyai\/ui(?:\/[^'"]+)?['"]|require\(\s*['"]@openkeyai\/ui(?:\/[^'"]+)?['"]/,
77
+ /** Mounted JSX: `<HubHeader …` (open tag). */
78
+ hubHeaderJsx: /<\s*HubHeader\b/,
79
+ /** Named import containing `HubHeader`. */
80
+ hubHeaderImport: /\bimport\b[^;\n]*?\bHubHeader\b[^;\n]*?from\s+['"]@openkeyai\/ui/,
81
+ /** Any import from `@openkeyai/sdk/_internal/*` — banned. */
82
+ bannedInternal: /from\s+['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]|require\(\s*['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]/
83
+ };
84
+ function isLayoutFile(relPath) {
85
+ const segments = relPath.split(/[\\/]/);
86
+ const base = segments[segments.length - 1] ?? "";
87
+ return /^layout\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
88
+ }
89
+ function* walkSourceFiles(root, dir) {
90
+ let entries;
91
+ try {
92
+ entries = readdirSync(dir, { withFileTypes: true });
93
+ } catch {
94
+ return;
95
+ }
96
+ for (const entry of entries) {
97
+ if (entry.isDirectory()) {
98
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
99
+ yield* walkSourceFiles(root, join(dir, entry.name));
100
+ continue;
101
+ }
102
+ if (!entry.isFile()) continue;
103
+ const idx = entry.name.lastIndexOf(".");
104
+ const ext = idx === -1 ? "" : entry.name.slice(idx).toLowerCase();
105
+ if (!SOURCE_EXTS.has(ext)) continue;
106
+ const absPath = join(dir, entry.name);
107
+ yield { relPath: relative(root, absPath), absPath };
108
+ }
109
+ }
110
+ function lineOf(text, regex) {
111
+ const m = regex.exec(text);
112
+ if (!m) return void 0;
113
+ const upto = text.slice(0, m.index);
114
+ return upto.split("\n").length;
115
+ }
116
+ function scanToolSource(input) {
117
+ const violations = [];
118
+ let scannedFileCount = 0;
119
+ let foundHubHeaderImport = false;
120
+ let foundHubHeaderMount = false;
121
+ let firstLayoutFile;
122
+ let srcExists = false;
123
+ try {
124
+ statSync(input.srcDir);
125
+ srcExists = true;
126
+ } catch {
127
+ violations.push({
128
+ code: "io-error",
129
+ message: `srcDir does not exist: ${input.srcDir}`
130
+ });
131
+ }
132
+ if (srcExists) {
133
+ for (const { relPath, absPath } of walkSourceFiles(
134
+ input.srcDir,
135
+ input.srcDir
136
+ )) {
137
+ scannedFileCount += 1;
138
+ let body;
139
+ try {
140
+ body = readFileSync(absPath, "utf8");
141
+ } catch (err) {
142
+ violations.push({
143
+ code: "io-error",
144
+ message: `Could not read file: ${err instanceof Error ? err.message : "unknown"}`,
145
+ file: relPath
146
+ });
147
+ continue;
148
+ }
149
+ const bannedLine = lineOf(body, PATTERNS.bannedInternal);
150
+ if (bannedLine !== void 0) {
151
+ violations.push({
152
+ code: "banned-internal-import",
153
+ message: "@openkeyai/sdk/_internal is not a public surface and may change without notice. Import only from the package root.",
154
+ file: relPath,
155
+ line: bannedLine
156
+ });
157
+ }
158
+ if (isLayoutFile(relPath)) {
159
+ if (firstLayoutFile === void 0) firstLayoutFile = relPath;
160
+ if (PATTERNS.hubHeaderImport.test(body)) {
161
+ foundHubHeaderImport = true;
162
+ }
163
+ if (PATTERNS.hubHeaderJsx.test(body)) {
164
+ foundHubHeaderMount = true;
165
+ }
166
+ } else if (PATTERNS.uiImport.test(body) && PATTERNS.hubHeaderJsx.test(body)) {
167
+ foundHubHeaderMount = true;
168
+ if (PATTERNS.hubHeaderImport.test(body)) {
169
+ foundHubHeaderImport = true;
170
+ }
171
+ }
172
+ }
173
+ if (!foundHubHeaderImport) {
174
+ violations.push({
175
+ code: "missing-hubheader-import",
176
+ message: "No layout file imports `HubHeader` from `@openkeyai/ui`. Every tool must mount the shared header.",
177
+ file: firstLayoutFile ?? "app/layout.tsx"
178
+ });
179
+ }
180
+ if (!foundHubHeaderMount) {
181
+ violations.push({
182
+ code: "missing-hubheader-mount",
183
+ message: "`<HubHeader />` is not mounted anywhere in the source tree. Place it in your root layout.",
184
+ file: firstLayoutFile ?? "app/layout.tsx"
185
+ });
186
+ }
187
+ }
188
+ if (input.manifestPath) {
189
+ try {
190
+ const raw = readFileSync(input.manifestPath, "utf8");
191
+ const json = JSON.parse(raw);
192
+ const parsed = ToolManifestSchema.safeParse(json);
193
+ if (!parsed.success) {
194
+ for (const issue of parsed.error.issues) {
195
+ violations.push({
196
+ code: "manifest-invalid",
197
+ message: `${issue.path.join(".") || "(root)"} \u2014 ${issue.message}`,
198
+ file: input.manifestPath
199
+ });
200
+ }
201
+ }
202
+ } catch (err) {
203
+ violations.push({
204
+ code: "manifest-missing",
205
+ message: `Could not read tool.json: ${err instanceof Error ? err.message : "unknown"}`,
206
+ file: input.manifestPath
207
+ });
208
+ }
209
+ }
210
+ return { violations, scannedFileCount };
211
+ }
212
+
213
+ // src/index.ts
214
+ var TOOL_MANIFEST_VERSION = "0.1.0";
215
+
216
+ export { ProviderSlugEnum, RuntimeTierEnum, TOOL_MANIFEST_VERSION, ToolManifestSchema, ToolOwnerSchema, ToolScopeEnum, scanToolSource, validateManifest, validateManifestFile };
217
+ //# sourceMappingURL=index.js.map
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/validate.ts","../src/scanner.ts","../src/index.ts"],"names":["readFileSync"],"mappings":"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","file":"index.js","sourcesContent":["import { z } from \"zod\";\n\n/**\n * `tool.json` — the manifest every OpenKey AI tool ships at the repo root.\n *\n * This schema is the SOURCE OF TRUTH for the contract. The hub's tool\n * registry sync (Phase 9b, lands in the hub repo) imports\n * `ToolManifestSchema` from this package and rejects any insert that\n * doesn't parse cleanly. The Phase 13 auto-scaffolder uses the same\n * schema to produce a starter `tool.json` for newly-voted tools.\n *\n * Status of fields:\n * - FROZEN — changing the shape (rename, type swap, required→optional)\n * is a major version bump with 60-day notice to tool authors\n * - ADDITIONS — new optional fields are minor version bumps\n *\n * We deliberately keep the schema lean. A field exists here only if either\n * (a) the hub needs it to register the tool, (b) the SDK needs it at\n * runtime, or (c) the scanner needs it to enforce a rule.\n */\n\n/** Slug — lowercase kebab-case, 3–40 chars. Matches the hub's `tools.slug` CHECK. */\nconst slugRegex = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;\n\n/** Scopes are the FROZEN set from ARCHITECTURE.md Appendix B. */\nexport const ToolScopeEnum = z.enum([\"keys.read\", \"user.read\", \"billing.read\"]);\nexport type ToolScope = z.infer<typeof ToolScopeEnum>;\n\n/** Provider slugs aligned with the hub vault's `PROVIDERS` registry. */\nexport const ProviderSlugEnum = z.enum([\n \"openai\",\n \"anthropic\",\n \"google\",\n \"replicate\",\n \"elevenlabs\",\n \"fal\",\n]);\nexport type ProviderSlug = z.infer<typeof ProviderSlugEnum>;\n\n/** Runtime tier — pick the right template + deploy pipeline. */\nexport const RuntimeTierEnum = z.enum([\"edge\", \"container\"]);\nexport type RuntimeTier = z.infer<typeof RuntimeTierEnum>;\n\n/** Owner — at least a name; email + github are nice-to-have for support. */\nexport const ToolOwnerSchema = z.object({\n name: z.string().min(1).max(80),\n email: z.string().email().optional(),\n github: z\n .string()\n .regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}[a-zA-Z0-9])?$/, \"github handle\")\n .optional(),\n});\nexport type ToolOwner = z.infer<typeof ToolOwnerSchema>;\n\n/** Top-level schema. */\nexport const ToolManifestSchema = z.object({\n /** Stable URL-safe identifier. Becomes the JWT's `aud` claim. */\n slug: z.string().regex(slugRegex, \"slug must be lowercase kebab-case, 3–40 chars\"),\n /** Human-readable name shown in the catalog and HubHeader. */\n name: z.string().min(1).max(80),\n /** One-sentence catalog description. */\n description: z.string().max(280).default(\"\"),\n /** Tool version (semver). */\n version: z.string().regex(/^\\d+\\.\\d+\\.\\d+(?:-[\\w.+-]+)?$/, \"semver\"),\n /** Runtime tier — picks template + deploy pipeline. */\n runtime: RuntimeTierEnum,\n /** Scopes the tool may request when minting a JWT. Min 1. */\n scopes: z.array(ToolScopeEnum).min(1),\n /** API provider slugs the tool may fetch keys for. */\n providers: z.array(ProviderSlugEnum).default([]),\n /** Public homepage / docs URL. Surfaced in the hub catalog. */\n homepage: z.string().url().optional(),\n /** Where the hub sends users after issuing a token. https only. */\n callback_url: z\n .string()\n .url()\n .refine((u) => u.startsWith(\"https://\"), \"callback_url must be https://\"),\n /** Tool owner. */\n owner: ToolOwnerSchema,\n /** Semver range the tool was built against. Hub uses this for compat warnings. */\n sdk_version: z.string().min(1),\n /** Optional category for catalog grouping (e.g. \"media\", \"writing\"). */\n category: z.string().min(1).max(40).optional(),\n});\n\nexport type ToolManifest = z.infer<typeof ToolManifestSchema>;\n\n/** Re-export Zod errors verbatim — consumers can `instanceof` check them. */\nexport { ZodError } from \"zod\";\n","import { readFileSync } from \"node:fs\";\nimport { ToolManifestSchema, type ToolManifest } from \"./schema\";\n\n/**\n * Parse + validate a `tool.json` payload. Pass either the parsed object or\n * the raw JSON string; we handle both.\n *\n * Returns the typed manifest on success.\n * Throws `ZodError` on schema failure (caller can `instanceof ZodError`).\n * Throws `Error` on JSON parse failure.\n */\nexport function validateManifest(input: unknown): ToolManifest {\n let json: unknown;\n if (typeof input === \"string\") {\n json = JSON.parse(input);\n } else {\n json = input;\n }\n return ToolManifestSchema.parse(json);\n}\n\n/**\n * Convenience — read + parse + validate a tool.json from disk.\n *\n * Used by the CLI and by the auto-scaffolder. Returns the manifest on\n * success; throws otherwise.\n */\nexport function validateManifestFile(path: string): ToolManifest {\n const raw = readFileSync(path, \"utf8\");\n return validateManifest(raw);\n}\n","import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { ToolManifestSchema } from \"./schema\";\n\n/**\n * Source scanner.\n *\n * Walks a tool's source tree and returns a list of contract violations.\n * The CLI (`okai-scan`) wraps this; tool CI calls it directly.\n *\n * Design notes:\n *\n * - **Regex / text scanning over AST.** A real TS AST parse (ts-morph)\n * catches more edge cases but adds ~5MB of deps and noticeable cold-\n * start time on every tool's CI. The patterns we enforce in v1 are\n * simple string matches; we're catching obvious violations, not\n * adversarial code. If we ever need to enforce something subtle (e.g.\n * \"you mounted HubHeader but inside a `{false && ...}` branch\") we\n * can swap the backend later — the public function signature won't\n * change.\n *\n * - **Heuristic file matching.** We treat any file matching\n * `**\\/layout.{ts,tsx,js,jsx,mjs,cjs}` as a \"layout\" candidate, which\n * covers both Next.js App Router (`app/layout.tsx`) and the variants\n * containers use (`src/layout.tsx`, etc).\n *\n * - **Single-pass walk.** Bigger tools have hundreds of source files;\n * doing one walk and routing each file to all rules keeps it linear.\n *\n * - **No file IO outside `srcDir`.** Safe to point at any directory;\n * we don't write anything.\n */\n\nexport type ScannerViolation = {\n /** Stable code so tool authors can suppress / look up. */\n code:\n | \"missing-hubheader-import\"\n | \"missing-hubheader-mount\"\n | \"banned-internal-import\"\n | \"manifest-missing\"\n | \"manifest-invalid\"\n | \"io-error\";\n /** Human-readable. Surface this in CI logs. */\n message: string;\n /** Relative path inside `srcDir` (or `tool.json` for manifest issues). */\n file?: string;\n /** 1-based line number for source-file violations. */\n line?: number;\n};\n\nexport type ScanInput = {\n /**\n * Directory containing the tool's source code (usually `src/` or `app/`\n * or the repo root). The scanner walks recursively.\n */\n srcDir: string;\n /**\n * Optional path to `tool.json`. If provided, the manifest is loaded and\n * validated against the schema; failures are reported as scanner\n * violations. Defaults to no manifest check (you can call\n * `validateManifestFile` directly).\n */\n manifestPath?: string;\n};\n\nexport type ScanResult = {\n violations: ScannerViolation[];\n /** Files actually walked — useful for CI logs. */\n scannedFileCount: number;\n};\n\n/** File extensions that contain JS/TS that we'll grep. */\nconst SOURCE_EXTS = new Set([\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"]);\n\n/** Directories to skip outright. */\nconst SKIP_DIRS = new Set([\n \"node_modules\",\n \"dist\",\n \".next\",\n \".open-next\",\n \".turbo\",\n \"build\",\n \"out\",\n \"coverage\",\n \".git\",\n]);\n\n/** Regex catalogues. Kept inline so they can be tweaked without breaking\n * the function signatures consumers depend on. */\nconst PATTERNS = {\n /** ESM/CJS import OR a `require()` of @openkeyai/ui. */\n uiImport: /\\bfrom\\s+['\"]@openkeyai\\/ui(?:\\/[^'\"]+)?['\"]|require\\(\\s*['\"]@openkeyai\\/ui(?:\\/[^'\"]+)?['\"]/,\n /** Mounted JSX: `<HubHeader …` (open tag). */\n hubHeaderJsx: /<\\s*HubHeader\\b/,\n /** Named import containing `HubHeader`. */\n hubHeaderImport: /\\bimport\\b[^;\\n]*?\\bHubHeader\\b[^;\\n]*?from\\s+['\"]@openkeyai\\/ui/,\n /** Any import from `@openkeyai/sdk/_internal/*` — banned. */\n bannedInternal: /from\\s+['\"]@openkeyai\\/sdk\\/_internal(?:\\/[^'\"]*)?['\"]|require\\(\\s*['\"]@openkeyai\\/sdk\\/_internal(?:\\/[^'\"]*)?['\"]/,\n} as const;\n\n/** Heuristic — is this file a Next.js / similar layout? */\nfunction isLayoutFile(relPath: string): boolean {\n // Match `…/layout.tsx`, `app/layout.ts`, etc.\n // Avoid `…/sublayout.tsx` collisions by requiring the basename to start with `layout`.\n const segments = relPath.split(/[\\\\/]/);\n const base = segments[segments.length - 1] ?? \"\";\n return /^layout\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);\n}\n\n/** Recursively yield (relPath, absPath) for every source file under `dir`. */\nfunction* walkSourceFiles(\n root: string,\n dir: string,\n): Generator<{ relPath: string; absPath: string }> {\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return; // unreadable dir — silently skip.\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(\".\")) continue;\n yield* walkSourceFiles(root, join(dir, entry.name));\n continue;\n }\n if (!entry.isFile()) continue;\n const idx = entry.name.lastIndexOf(\".\");\n const ext = idx === -1 ? \"\" : entry.name.slice(idx).toLowerCase();\n if (!SOURCE_EXTS.has(ext)) continue;\n const absPath = join(dir, entry.name);\n yield { relPath: relative(root, absPath), absPath };\n }\n}\n\n/** Find the 1-based line of the first match of `regex` in `text`. */\nfunction lineOf(text: string, regex: RegExp): number | undefined {\n const m = regex.exec(text);\n if (!m) return undefined;\n const upto = text.slice(0, m.index);\n return upto.split(\"\\n\").length;\n}\n\n/**\n * Scan a tool's source tree for contract violations.\n *\n * This function makes no assumptions about the tool's framework. It just\n * walks `srcDir`, applies a small set of rules, and reports what it sees.\n */\nexport function scanToolSource(input: ScanInput): ScanResult {\n const violations: ScannerViolation[] = [];\n let scannedFileCount = 0;\n\n let foundHubHeaderImport = false;\n let foundHubHeaderMount = false;\n /** First layout file we saw — used as the location for the \"no mount\" violation. */\n let firstLayoutFile: string | undefined;\n\n let srcExists = false;\n try {\n statSync(input.srcDir);\n srcExists = true;\n } catch {\n violations.push({\n code: \"io-error\",\n message: `srcDir does not exist: ${input.srcDir}`,\n });\n }\n\n if (srcExists) {\n for (const { relPath, absPath } of walkSourceFiles(\n input.srcDir,\n input.srcDir,\n )) {\n scannedFileCount += 1;\n let body: string;\n try {\n body = readFileSync(absPath, \"utf8\");\n } catch (err) {\n violations.push({\n code: \"io-error\",\n message: `Could not read file: ${err instanceof Error ? err.message : \"unknown\"}`,\n file: relPath,\n });\n continue;\n }\n\n // Rule 1: banned _internal import — ANY file.\n const bannedLine = lineOf(body, PATTERNS.bannedInternal);\n if (bannedLine !== undefined) {\n violations.push({\n code: \"banned-internal-import\",\n message:\n \"@openkeyai/sdk/_internal is not a public surface and may change without notice. Import only from the package root.\",\n file: relPath,\n line: bannedLine,\n });\n }\n\n // Rules 2-3: HubHeader presence — only meaningful in layout files.\n if (isLayoutFile(relPath)) {\n if (firstLayoutFile === undefined) firstLayoutFile = relPath;\n if (PATTERNS.hubHeaderImport.test(body)) {\n foundHubHeaderImport = true;\n }\n if (PATTERNS.hubHeaderJsx.test(body)) {\n foundHubHeaderMount = true;\n }\n } else if (PATTERNS.uiImport.test(body) && PATTERNS.hubHeaderJsx.test(body)) {\n // HubHeader mounted in a non-layout file. That's allowed (it's still\n // visible) — count it as a mount but no separate rule.\n foundHubHeaderMount = true;\n if (PATTERNS.hubHeaderImport.test(body)) {\n foundHubHeaderImport = true;\n }\n }\n }\n\n if (!foundHubHeaderImport) {\n violations.push({\n code: \"missing-hubheader-import\",\n message:\n \"No layout file imports `HubHeader` from `@openkeyai/ui`. Every tool must mount the shared header.\",\n file: firstLayoutFile ?? \"app/layout.tsx\",\n });\n }\n if (!foundHubHeaderMount) {\n violations.push({\n code: \"missing-hubheader-mount\",\n message:\n \"`<HubHeader />` is not mounted anywhere in the source tree. Place it in your root layout.\",\n file: firstLayoutFile ?? \"app/layout.tsx\",\n });\n }\n }\n\n // Manifest check (optional path).\n if (input.manifestPath) {\n try {\n const raw = readFileSync(input.manifestPath, \"utf8\");\n const json = JSON.parse(raw);\n const parsed = ToolManifestSchema.safeParse(json);\n if (!parsed.success) {\n for (const issue of parsed.error.issues) {\n violations.push({\n code: \"manifest-invalid\",\n message: `${issue.path.join(\".\") || \"(root)\"} — ${issue.message}`,\n file: input.manifestPath,\n });\n }\n }\n } catch (err) {\n violations.push({\n code: \"manifest-missing\",\n message: `Could not read tool.json: ${err instanceof Error ? err.message : \"unknown\"}`,\n file: input.manifestPath,\n });\n }\n }\n\n return { violations, scannedFileCount };\n}\n","/**\n * `@openkeyai/tool-manifest` — public entry.\n *\n * Three surfaces:\n *\n * 1. The Zod schema (`ToolManifestSchema`) — frozen shape of every\n * tool's `tool.json`. Re-exported via the `./schema` subpath too,\n * so consumers that ONLY need the schema can avoid pulling the\n * filesystem-touching scanner code.\n *\n * 2. `validateManifest(json)` / `validateManifestFile(path)` —\n * convenience wrappers around `ToolManifestSchema.parse`.\n *\n * 3. `scanToolSource({ srcDir, manifestPath? })` — walks the tool's\n * source tree and returns contract violations. Used by tool CI\n * and the auto-scaffolder.\n *\n * The CLI (`okai-scan`) is a thin wrapper around these — `bin/cli.ts`.\n */\n\nexport {\n ToolManifestSchema,\n ProviderSlugEnum,\n ToolScopeEnum,\n RuntimeTierEnum,\n ToolOwnerSchema,\n ZodError,\n} from \"./schema\";\nexport type {\n ToolManifest,\n ToolScope,\n ProviderSlug,\n RuntimeTier,\n ToolOwner,\n} from \"./schema\";\n\nexport { validateManifest, validateManifestFile } from \"./validate\";\n\nexport { scanToolSource } from \"./scanner\";\nexport type { ScanInput, ScanResult, ScannerViolation } from \"./scanner\";\n\n/** Bumped on each release. */\nexport const TOOL_MANIFEST_VERSION = \"0.1.0\";\n"]}
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // src/schema.ts
6
+ var slugRegex = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
7
+ var ToolScopeEnum = zod.z.enum(["keys.read", "user.read", "billing.read"]);
8
+ var ProviderSlugEnum = zod.z.enum([
9
+ "openai",
10
+ "anthropic",
11
+ "google",
12
+ "replicate",
13
+ "elevenlabs",
14
+ "fal"
15
+ ]);
16
+ var RuntimeTierEnum = zod.z.enum(["edge", "container"]);
17
+ var ToolOwnerSchema = zod.z.object({
18
+ name: zod.z.string().min(1).max(80),
19
+ email: zod.z.string().email().optional(),
20
+ github: zod.z.string().regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}[a-zA-Z0-9])?$/, "github handle").optional()
21
+ });
22
+ var ToolManifestSchema = zod.z.object({
23
+ /** Stable URL-safe identifier. Becomes the JWT's `aud` claim. */
24
+ slug: zod.z.string().regex(slugRegex, "slug must be lowercase kebab-case, 3\u201340 chars"),
25
+ /** Human-readable name shown in the catalog and HubHeader. */
26
+ name: zod.z.string().min(1).max(80),
27
+ /** One-sentence catalog description. */
28
+ description: zod.z.string().max(280).default(""),
29
+ /** Tool version (semver). */
30
+ version: zod.z.string().regex(/^\d+\.\d+\.\d+(?:-[\w.+-]+)?$/, "semver"),
31
+ /** Runtime tier — picks template + deploy pipeline. */
32
+ runtime: RuntimeTierEnum,
33
+ /** Scopes the tool may request when minting a JWT. Min 1. */
34
+ scopes: zod.z.array(ToolScopeEnum).min(1),
35
+ /** API provider slugs the tool may fetch keys for. */
36
+ providers: zod.z.array(ProviderSlugEnum).default([]),
37
+ /** Public homepage / docs URL. Surfaced in the hub catalog. */
38
+ homepage: zod.z.string().url().optional(),
39
+ /** Where the hub sends users after issuing a token. https only. */
40
+ callback_url: zod.z.string().url().refine((u) => u.startsWith("https://"), "callback_url must be https://"),
41
+ /** Tool owner. */
42
+ owner: ToolOwnerSchema,
43
+ /** Semver range the tool was built against. Hub uses this for compat warnings. */
44
+ sdk_version: zod.z.string().min(1),
45
+ /** Optional category for catalog grouping (e.g. "media", "writing"). */
46
+ category: zod.z.string().min(1).max(40).optional()
47
+ });
48
+
49
+ Object.defineProperty(exports, "ZodError", {
50
+ enumerable: true,
51
+ get: function () { return zod.ZodError; }
52
+ });
53
+ exports.ProviderSlugEnum = ProviderSlugEnum;
54
+ exports.RuntimeTierEnum = RuntimeTierEnum;
55
+ exports.ToolManifestSchema = ToolManifestSchema;
56
+ exports.ToolOwnerSchema = ToolOwnerSchema;
57
+ exports.ToolScopeEnum = ToolScopeEnum;
58
+ //# sourceMappingURL=schema.cjs.map
59
+ //# sourceMappingURL=schema.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts"],"names":["z"],"mappings":";;;;;AAsBA,IAAM,SAAA,GAAY,gCAAA;AAGX,IAAM,gBAAgBA,KAAA,CAAE,IAAA,CAAK,CAAC,WAAA,EAAa,WAAA,EAAa,cAAc,CAAC;AAIvE,IAAM,gBAAA,GAAmBA,MAAE,IAAA,CAAK;AAAA,EACrC,QAAA;AAAA,EACA,WAAA;AAAA,EACA,QAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAC;AAIM,IAAM,kBAAkBA,KAAA,CAAE,IAAA,CAAK,CAAC,MAAA,EAAQ,WAAW,CAAC;AAIpD,IAAM,eAAA,GAAkBA,MAAE,MAAA,CAAO;AAAA,EACtC,IAAA,EAAMA,MAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,EAAE,CAAA;AAAA,EAC9B,OAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,KAAA,GAAQ,QAAA,EAAS;AAAA,EACnC,MAAA,EAAQA,MACL,MAAA,EAAO,CACP,MAAM,iDAAA,EAAmD,eAAe,EACxE,QAAA;AACL,CAAC;AAIM,IAAM,kBAAA,GAAqBA,MAAE,MAAA,CAAO;AAAA;AAAA,EAEzC,MAAMA,KAAA,CAAE,MAAA,EAAO,CAAE,KAAA,CAAM,WAAW,oDAA+C,CAAA;AAAA;AAAA,EAEjF,IAAA,EAAMA,MAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,EAAE,CAAA;AAAA;AAAA,EAE9B,WAAA,EAAaA,MAAE,MAAA,EAAO,CAAE,IAAI,GAAG,CAAA,CAAE,QAAQ,EAAE,CAAA;AAAA;AAAA,EAE3C,SAASA,KAAA,CAAE,MAAA,EAAO,CAAE,KAAA,CAAM,iCAAiC,QAAQ,CAAA;AAAA;AAAA,EAEnE,OAAA,EAAS,eAAA;AAAA;AAAA,EAET,QAAQA,KAAA,CAAE,KAAA,CAAM,aAAa,CAAA,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEpC,WAAWA,KAAA,CAAE,KAAA,CAAM,gBAAgB,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA;AAAA,EAE/C,UAAUA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,QAAA,EAAS;AAAA;AAAA,EAEpC,YAAA,EAAcA,KAAA,CACX,MAAA,EAAO,CACP,GAAA,EAAI,CACJ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,UAAA,CAAW,UAAU,GAAG,+BAA+B,CAAA;AAAA;AAAA,EAE1E,KAAA,EAAO,eAAA;AAAA;AAAA,EAEP,WAAA,EAAaA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAE7B,QAAA,EAAUA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAC,CAAA,CAAE,GAAA,CAAI,EAAE,CAAA,CAAE,QAAA;AACtC,CAAC","file":"schema.cjs","sourcesContent":["import { z } from \"zod\";\n\n/**\n * `tool.json` — the manifest every OpenKey AI tool ships at the repo root.\n *\n * This schema is the SOURCE OF TRUTH for the contract. The hub's tool\n * registry sync (Phase 9b, lands in the hub repo) imports\n * `ToolManifestSchema` from this package and rejects any insert that\n * doesn't parse cleanly. The Phase 13 auto-scaffolder uses the same\n * schema to produce a starter `tool.json` for newly-voted tools.\n *\n * Status of fields:\n * - FROZEN — changing the shape (rename, type swap, required→optional)\n * is a major version bump with 60-day notice to tool authors\n * - ADDITIONS — new optional fields are minor version bumps\n *\n * We deliberately keep the schema lean. A field exists here only if either\n * (a) the hub needs it to register the tool, (b) the SDK needs it at\n * runtime, or (c) the scanner needs it to enforce a rule.\n */\n\n/** Slug — lowercase kebab-case, 3–40 chars. Matches the hub's `tools.slug` CHECK. */\nconst slugRegex = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;\n\n/** Scopes are the FROZEN set from ARCHITECTURE.md Appendix B. */\nexport const ToolScopeEnum = z.enum([\"keys.read\", \"user.read\", \"billing.read\"]);\nexport type ToolScope = z.infer<typeof ToolScopeEnum>;\n\n/** Provider slugs aligned with the hub vault's `PROVIDERS` registry. */\nexport const ProviderSlugEnum = z.enum([\n \"openai\",\n \"anthropic\",\n \"google\",\n \"replicate\",\n \"elevenlabs\",\n \"fal\",\n]);\nexport type ProviderSlug = z.infer<typeof ProviderSlugEnum>;\n\n/** Runtime tier — pick the right template + deploy pipeline. */\nexport const RuntimeTierEnum = z.enum([\"edge\", \"container\"]);\nexport type RuntimeTier = z.infer<typeof RuntimeTierEnum>;\n\n/** Owner — at least a name; email + github are nice-to-have for support. */\nexport const ToolOwnerSchema = z.object({\n name: z.string().min(1).max(80),\n email: z.string().email().optional(),\n github: z\n .string()\n .regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}[a-zA-Z0-9])?$/, \"github handle\")\n .optional(),\n});\nexport type ToolOwner = z.infer<typeof ToolOwnerSchema>;\n\n/** Top-level schema. */\nexport const ToolManifestSchema = z.object({\n /** Stable URL-safe identifier. Becomes the JWT's `aud` claim. */\n slug: z.string().regex(slugRegex, \"slug must be lowercase kebab-case, 3–40 chars\"),\n /** Human-readable name shown in the catalog and HubHeader. */\n name: z.string().min(1).max(80),\n /** One-sentence catalog description. */\n description: z.string().max(280).default(\"\"),\n /** Tool version (semver). */\n version: z.string().regex(/^\\d+\\.\\d+\\.\\d+(?:-[\\w.+-]+)?$/, \"semver\"),\n /** Runtime tier — picks template + deploy pipeline. */\n runtime: RuntimeTierEnum,\n /** Scopes the tool may request when minting a JWT. Min 1. */\n scopes: z.array(ToolScopeEnum).min(1),\n /** API provider slugs the tool may fetch keys for. */\n providers: z.array(ProviderSlugEnum).default([]),\n /** Public homepage / docs URL. Surfaced in the hub catalog. */\n homepage: z.string().url().optional(),\n /** Where the hub sends users after issuing a token. https only. */\n callback_url: z\n .string()\n .url()\n .refine((u) => u.startsWith(\"https://\"), \"callback_url must be https://\"),\n /** Tool owner. */\n owner: ToolOwnerSchema,\n /** Semver range the tool was built against. Hub uses this for compat warnings. */\n sdk_version: z.string().min(1),\n /** Optional category for catalog grouping (e.g. \"media\", \"writing\"). */\n category: z.string().min(1).max(40).optional(),\n});\n\nexport type ToolManifest = z.infer<typeof ToolManifestSchema>;\n\n/** Re-export Zod errors verbatim — consumers can `instanceof` check them. */\nexport { ZodError } from \"zod\";\n"]}
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ export { ZodError } from 'zod';
3
+
4
+ /** Scopes are the FROZEN set from ARCHITECTURE.md Appendix B. */
5
+ declare const ToolScopeEnum: z.ZodEnum<{
6
+ "keys.read": "keys.read";
7
+ "user.read": "user.read";
8
+ "billing.read": "billing.read";
9
+ }>;
10
+ type ToolScope = z.infer<typeof ToolScopeEnum>;
11
+ /** Provider slugs aligned with the hub vault's `PROVIDERS` registry. */
12
+ declare const ProviderSlugEnum: z.ZodEnum<{
13
+ openai: "openai";
14
+ anthropic: "anthropic";
15
+ google: "google";
16
+ replicate: "replicate";
17
+ elevenlabs: "elevenlabs";
18
+ fal: "fal";
19
+ }>;
20
+ type ProviderSlug = z.infer<typeof ProviderSlugEnum>;
21
+ /** Runtime tier — pick the right template + deploy pipeline. */
22
+ declare const RuntimeTierEnum: z.ZodEnum<{
23
+ edge: "edge";
24
+ container: "container";
25
+ }>;
26
+ type RuntimeTier = z.infer<typeof RuntimeTierEnum>;
27
+ /** Owner — at least a name; email + github are nice-to-have for support. */
28
+ declare const ToolOwnerSchema: z.ZodObject<{
29
+ name: z.ZodString;
30
+ email: z.ZodOptional<z.ZodString>;
31
+ github: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strip>;
33
+ type ToolOwner = z.infer<typeof ToolOwnerSchema>;
34
+ /** Top-level schema. */
35
+ declare const ToolManifestSchema: z.ZodObject<{
36
+ slug: z.ZodString;
37
+ name: z.ZodString;
38
+ description: z.ZodDefault<z.ZodString>;
39
+ version: z.ZodString;
40
+ runtime: z.ZodEnum<{
41
+ edge: "edge";
42
+ container: "container";
43
+ }>;
44
+ scopes: z.ZodArray<z.ZodEnum<{
45
+ "keys.read": "keys.read";
46
+ "user.read": "user.read";
47
+ "billing.read": "billing.read";
48
+ }>>;
49
+ providers: z.ZodDefault<z.ZodArray<z.ZodEnum<{
50
+ openai: "openai";
51
+ anthropic: "anthropic";
52
+ google: "google";
53
+ replicate: "replicate";
54
+ elevenlabs: "elevenlabs";
55
+ fal: "fal";
56
+ }>>>;
57
+ homepage: z.ZodOptional<z.ZodString>;
58
+ callback_url: z.ZodString;
59
+ owner: z.ZodObject<{
60
+ name: z.ZodString;
61
+ email: z.ZodOptional<z.ZodString>;
62
+ github: z.ZodOptional<z.ZodString>;
63
+ }, z.core.$strip>;
64
+ sdk_version: z.ZodString;
65
+ category: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strip>;
67
+ type ToolManifest = z.infer<typeof ToolManifestSchema>;
68
+
69
+ export { type ProviderSlug, ProviderSlugEnum, type RuntimeTier, RuntimeTierEnum, type ToolManifest, ToolManifestSchema, type ToolOwner, ToolOwnerSchema, type ToolScope, ToolScopeEnum };